Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 817
0.00% covered (danger)
0.00%
0 / 56
CRAP
0.00% covered (danger)
0.00%
0 / 1
tao_actions_RdfController
0.00% covered (danger)
0.00%
0 / 817
0.00% covered (danger)
0.00%
0 / 56
39006
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getClassService
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 validateInstanceRoot
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 validateInstancesRoot
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isLocked
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getCurrentClass
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 getCurrentInstance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 validateUri
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getRootClass
n/a
0 / 0
n/a
0 / 0
0
 editClassProperties
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 editClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClassForm
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOntologyData
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 getTreeOptionsFromRequest
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 addPermissions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 computePermissions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 editClassLabel
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 addInstance
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 addSubClass
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 addInstanceForm
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 createInstance
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 editInstance
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 cloneInstance
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 copyInstance
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
56
 copyClass
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
30
 moveInstance
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
56
 moveResource
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
20
 moveAll
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 translateInstance
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
240
 getTranslatedData
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 delete
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
20
 deleteResource
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 deleteClass
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
30
 deleteAll
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
90
 addClassProperty
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 removeClassProperty
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 hasWriteAccess
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 validateMoveRequest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 moveAllInstances
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getInstancesList
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
20
 getInstancesToMove
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 move
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 returnJsonError
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getExtraValidationRules
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExcludedProperties
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getActionService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSignatureGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createFormSignature
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 validateDestinationClass
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 getEventManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClassDeleter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceDeleter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueueDispatcher
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceTransfer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResourceParentClass
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2002-2008 (original work) Public Research Centre Henri Tudor & University of Luxembourg
19 *                         (under the project TAO & TAO2);
20 *               2008-2010 (update and modification) Deutsche Institut für Internationale Pädagogische Forschung
21 *                         (under the project TAO-TRANSFER);
22 *               2009-2012 (update and modification) Public Research Centre Henri Tudor
23 *                         (under the project TAO-SUSTAIN & TAO-DEV);
24 *               2013-2022 (update and modification) Open Assessment Technologies SA.
25 */
26
27use oat\oatbox\user\User;
28use oat\oatbox\event\EventManager;
29use oat\generis\model\OntologyRdfs;
30use oat\tao\model\lock\LockManager;
31use oat\tao\model\menu\MenuService;
32use oat\tao\model\menu\ActionService;
33use oat\tao\model\resources\Command\ResourceTransferCommand;
34use oat\tao\model\resources\Contract\ResourceTransferInterface;
35use oat\tao\model\resources\Service\ResourceTransferProxy;
36use oat\tao\model\task\CopyClassTask;
37use oat\generis\model\OntologyAwareTrait;
38use oat\tao\model\search\tasks\IndexTrait;
39use oat\tao\model\event\ResourceMovedEvent;
40use oat\tao\model\resources\ResourceService;
41use oat\tao\model\security\SecurityException;
42use oat\tao\model\security\SignatureGenerator;
43use oat\tao\model\security\SignatureValidator;
44use oat\tao\model\taskQueue\TaskLogActionTrait;
45use oat\tao\model\controller\SignedFormInstance;
46use oat\tao\model\resources\Service\ClassDeleter;
47use oat\tao\model\accessControl\PermissionChecker;
48use tao_helpers_form_FormContainer as FormContainer;
49use oat\tao\model\taskQueue\QueueDispatcherInterface;
50use oat\generis\model\resource\Service\ResourceDeleter;
51use oat\tao\model\ClassProperty\RemoveClassPropertyService;
52use oat\tao\model\resources\Contract\ClassDeleterInterface;
53use oat\tao\model\ClassProperty\AddClassPropertyFormFactory;
54use oat\tao\model\resources\Exception\ClassDeletionException;
55use oat\generis\model\resource\Contract\ResourceDeleterInterface;
56use oat\tao\model\metadata\exception\InconsistencyConfigException;
57use oat\tao\model\resources\Exception\PartialClassDeletionException;
58
59/**
60 * The TaoModule is an abstract controller,
61 * the tao children extensions Modules should extends the TaoModule to beneficiate the shared methods.
62 * It regroups the methods that can be applied on any extension (the rdf:Class managment for example)
63 *
64 * @author CRP Henri Tudor - TAO Team - {@link http://www.tao.lu}
65 * @license GPLv2  http://www.opensource.org/licenses/gpl-2.0.php
66 *
67 * phpcs:disable Squiz.Classes.ValidClassName
68 */
69abstract class tao_actions_RdfController extends tao_actions_CommonModule
70{
71    use OntologyAwareTrait;
72    use TaskLogActionTrait;
73    use IndexTrait;
74
75    /** @var SignatureValidator */
76    protected $signatureValidator;
77
78    public function __construct()
79    {
80        // maybe some day we can inject it
81        $this->signatureValidator = new SignatureValidator();
82
83        parent::__construct();
84    }
85
86    /**
87     * The Modules access the models throught the service instance
88     *
89     * @var tao_models_classes_Service
90     */
91    protected $service = null;
92
93    /**
94     * @return tao_models_classes_ClassService
95     */
96    protected function getClassService()
97    {
98        if (is_null($this->service)) {
99            throw new common_exception_Error('No service defined for ' . get_called_class());
100        }
101        return $this->service;
102    }
103
104    /**
105     * @param string $uri
106     *
107     * @throws common_exception_Error
108     * @throws SecurityException
109     */
110    protected function validateInstanceRoot($uri)
111    {
112        $instance = $this->getResource($uri);
113
114        $root = $this->getRootClass();
115
116        if ($instance->isClass()) {
117            $class = new core_kernel_classes_Class($instance->getUri());
118
119            if (!($class->isSubClassOf($root) || $class->equals($root))) {
120                throw new SecurityException(
121                    sprintf(
122                        'Security issue: class %s is not a subclass of %s',
123                        $instance->getLabel(),
124                        $root->getLabel()
125                    )
126                );
127            }
128
129            return;
130        }
131
132        if (!$instance->isInstanceOf($root)) {
133            throw new SecurityException(sprintf(
134                'Security issue: instance %s is not a child of %s',
135                $instance->getLabel(),
136                $root->getLabel()
137            ));
138        }
139    }
140
141    protected function validateInstancesRoot($uris)
142    {
143        foreach ($uris as $uri) {
144            $this->validateInstanceRoot($uri);
145        }
146    }
147
148    /**
149     * If you want strictly to check if the resource is locked,
150     * you should use LockManager::getImplementation()->isLocked($resource)
151     * Controller level convenience method to check if @resource is being locked, prepare data ans sets view,
152     *
153     * @param core_kernel_classes_Resource $resource
154     * @param $view
155     *
156     * @return boolean
157     */
158    protected function isLocked($resource, $view = null)
159    {
160
161        $lock = LockManager::getImplementation()->getLockData($resource);
162        if (!is_null($lock) && $lock->getOwnerId() != $this->getSession()->getUser()->getIdentifier()) {
163            //if (LockManager::getImplementation()->isLocked($resource)) {
164            $params = [
165                'id' => $resource->getUri(),
166                'topclass-label' => $this->getRootClass()->getLabel()
167            ];
168            if (!is_null($view)) {
169                $params['view'] = $view;
170                $params['ext'] = Context::getInstance()->getExtensionName();
171            }
172            $this->forward('locked', 'Lock', 'tao', $params);
173        }
174        return false;
175    }
176
177    /**
178     * get the current item class regarding the classUri' request parameter
179     * @return core_kernel_classes_Class the item class
180     * @throws Exception
181     */
182    protected function getCurrentClass()
183    {
184        $classUri = tao_helpers_Uri::decode($this->getRequestParameter('classUri'));
185        if (is_null($classUri) || empty($classUri)) {
186            $class = null;
187            $resource = $this->getCurrentInstance();
188            foreach ($resource->getTypes() as $type) {
189                $class = $type;
190                break;
191            }
192            if (is_null($class)) {
193                throw new Exception("No valid class uri found");
194            }
195            $returnValue = $class;
196        } else {
197            if (!common_Utils::isUri($classUri)) {
198                throw new tao_models_classes_MissingRequestParameterException('classUri - expected to be valid URI');
199            }
200            $returnValue = $this->getClass($classUri);
201        }
202
203        return $returnValue;
204    }
205
206    /**
207     *  ! Please override me !
208     * get the current instance regarding the uri and classUri in parameter
209     * @param string $parameterName
210     *
211     * @return core_kernel_classes_Resource
212     * @throws tao_models_classes_MissingRequestParameterException
213     */
214    protected function getCurrentInstance($parameterName = 'uri')
215    {
216        $uri = tao_helpers_Uri::decode($this->getRequestParameter($parameterName));
217
218        $this->validateUri($uri);
219
220        return $this->getResource($uri);
221    }
222
223    protected function validateUri($uri)
224    {
225        if (!common_Utils::isUri($uri)) {
226            throw new tao_models_classes_MissingRequestParameterException('uri');
227        }
228    }
229
230    /**
231     * get the main class
232     * @return core_kernel_classes_Class
233     */
234    abstract protected function getRootClass();
235
236    public function editClassProperties()
237    {
238        return $this->forward('index', 'PropertiesAuthoring', 'tao');
239    }
240
241    /**
242     * Deprecated alias for getClassForm
243     *
244     * @deprecated
245     */
246    protected function editClass(
247        core_kernel_classes_Class $class,
248        core_kernel_classes_Resource $resource,
249        core_kernel_classes_Class $topclass = null
250    ) {
251        return $this->getClassForm($class, $resource, $topclass);
252    }
253
254    protected function getClassForm($class, $resource, $topclass = null)
255    {
256        $controller = new tao_actions_PropertiesAuthoring();
257        $controller->setServiceLocator($this->getServiceLocator());
258        return $controller->getClassForm($class);
259    }
260
261    /*
262     * Actions
263     */
264
265    /**
266     * Main action
267     * @return void
268     */
269    public function index()
270    {
271        $this->setView('index.tpl');
272    }
273
274    /**
275     * Renders json data from the current ontology root class.
276     *
277     * The possible request parameters are the following:
278     *
279     * * uniqueNode: A URI indicating the returned hierarchy will be a single class, with a single children
280     *               corresponding to the URI.
281     * * browse:
282     * * hideInstances:
283     * * chunk:
284     * * offset:
285     * * limit:
286     * * subclasses:
287     * * classUri:
288     *
289     * @return void
290     * @requiresRight classUri READ
291     */
292    public function getOntologyData()
293    {
294        if (!$this->isXmlHttpRequest()) {
295            throw new common_exception_IsAjaxAction(__FUNCTION__);
296        }
297        $options = $this->getTreeOptionsFromRequest([]);
298
299        //generate the tree from the given parameters
300        $tree = $this->getClassService()->toTree($options['class'], $options);
301
302        //retrieve resources permissions
303        $user = \common_Session_SessionManager::getSession()->getUser();
304        $permissions = $this->getResourceService()->getResourcesPermissions($user, $tree);
305
306        //expose the tree
307        $this->returnJson([
308            'tree' => $tree,
309            'permissions' => $permissions
310        ]);
311    }
312
313    /**
314     * Get options to generate tree
315     * @return array
316     * @throws Exception
317     */
318    protected function getTreeOptionsFromRequest($options = [])
319    {
320        $options = array_merge([
321            'subclasses' => true,
322            'instances' => true,
323            'highlightUri' => '',
324            'chunk' => false,
325            'offset' => 0,
326            'limit' => 0
327        ], $options);
328
329        if ($this->hasRequestParameter('loadNode')) {
330            $options['uniqueNode'] = $this->getRequestParameter('loadNode');
331        }
332
333        if ($this->hasRequestParameter("selected")) {
334            $options['browse'] = [$this->getRequestParameter("selected")];
335        }
336
337        if ($this->hasRequestParameter('hideInstances')) {
338            if ((bool) $this->getRequestParameter('hideInstances')) {
339                $options['instances'] = false;
340            }
341        }
342        if ($this->hasRequestParameter('classUri')) {
343            $options['class'] = $this->getCurrentClass();
344            $options['chunk'] = !$options['class']->equals($this->getRootClass());
345        } else {
346            $options['class'] = $this->getRootClass();
347        }
348
349        if ($this->hasRequestParameter('offset')) {
350            $options['offset'] = $this->getRequestParameter('offset');
351        }
352
353        if ($this->hasRequestParameter('limit')) {
354            $options['limit'] = $this->getRequestParameter('limit');
355        }
356
357        if ($this->hasRequestParameter('order')) {
358            $options['order'] = tao_helpers_Uri::decode($this->getRequestParameter('order'));
359        }
360
361        if ($this->hasRequestParameter('orderdir')) {
362            $options['orderdir'] = $this->getRequestParameter('orderdir');
363        }
364        return $options;
365    }
366
367    /**
368     * Add permission information to the tree structure
369     *
370     * @deprecated
371     *
372     * @param array $tree
373     * @return array
374     */
375    protected function addPermissions($tree)
376    {
377        $user = $this->getSession()->getUser();
378
379        $section = MenuService::getSection(
380            $this->getRequestParameter('extension'),
381            $this->getRequestParameter('perspective'),
382            $this->getRequestParameter('section')
383        );
384
385        $actions = $section->getActions();
386
387        //then compute ACL for each node of the tree
388        $treeKeys = array_keys($tree);
389        if (isset($treeKeys[0]) && is_int($treeKeys[0])) {
390            foreach ($tree as $index => $treeNode) {
391                $tree[$index] = $this->computePermissions($actions, $user, $treeNode);
392            }
393        } else {
394            $tree = $this->computePermissions($actions, $user, $tree);
395        }
396
397        return $tree;
398    }
399
400    /**
401     * compulte permissions for a node against actions
402     *
403     * @deprecated
404     *
405     * @param array[] $actions the actions data with context, name and the resolver
406     * @param User $user the user
407     * @param array $node a tree node
408     * @return array the node augmented with permissions
409     */
410    private function computePermissions($actions, $user, $node)
411    {
412        if (isset($node['attributes']['data-uri'])) {
413            if ($node['type'] == 'class') {
414                $params = ['classUri' => $node['attributes']['data-uri']];
415            } else {
416                $params = [];
417                foreach ($node['attributes'] as $key => $value) {
418                    if (substr($key, 0, strlen('data-')) == 'data-') {
419                        $params[substr($key, strlen('data-'))] = $value;
420                    }
421                }
422            }
423            $params['id'] = $node['attributes']['data-uri'];
424
425            $node['permissions'] = $this->getActionService()->computePermissions($actions, $user, $params);
426        }
427        if (isset($node['children'])) {
428            foreach ($node['children'] as $index => $child) {
429                $node['children'][$index] = $this->computePermissions($actions, $user, $child);
430            }
431        }
432        return $node;
433    }
434
435    /**
436     * Common action to view and change the label of a class
437     */
438    public function editClassLabel()
439    {
440        $class = $this->getCurrentClass();
441        $signature = $this->createFormSignature();
442
443        $classUri = $class->getUri();
444        $hasWriteAccess = $this->hasWriteAccess($classUri) && $this->hasWriteAccessToAction(__FUNCTION__);
445
446        $editClassLabelForm = new tao_actions_form_EditClassLabel(
447            $class,
448            $this->getRequestParameters(),
449            $signature,
450            [FormContainer::CSRF_PROTECTION_OPTION => true, FormContainer::IS_DISABLED => !$hasWriteAccess]
451        );
452
453        $myForm = $editClassLabelForm->getForm();
454
455        if ($myForm->isSubmited() && $myForm->isValid()) {
456            if ($hasWriteAccess) {
457                $labelFormElement = $myForm->getElement(tao_helpers_Uri::encode(OntologyRdfs::RDFS_LABEL));
458                $labelFormElement && !$labelFormElement->isDisabled() && $class->setLabel(
459                    $myForm->getValue(tao_helpers_Uri::encode(OntologyRdfs::RDFS_LABEL))
460                );
461                $this->setData('message', __('%s Class saved', $class->getLabel()));
462            } else {
463                $this->setData('errorMessage', __('You do not have the required rights to edit this resource.'));
464            }
465
466            $this->setData('selectNode', tao_helpers_Uri::encode($classUri));
467            $this->setData('reload', true);
468        }
469
470        $this->setData('formTitle', __('Edit class %s', \tao_helpers_Display::htmlize($class->getLabel())));
471        $this->setData('myForm', $myForm->render());
472        $this->setView('form.tpl', 'tao');
473    }
474
475    /**
476     * Add an instance of the selected class
477     *
478     * @requiresRight id WRITE
479     * @requiresRight classUri WRITE
480     *
481     * @throws SecurityException
482     * @throws InconsistencyConfigException
483     * @throws common_exception_BadRequest
484     * @throws common_exception_Error
485     */
486    public function addInstance()
487    {
488        if (!$this->isXmlHttpRequest()) {
489            throw new common_exception_BadRequest('wrong request mode');
490        }
491
492        $id = $this->getRequestParameter('id');
493
494        $this->validateInstanceRoot($id);
495
496        try {
497            $this->validateCsrf();
498        } catch (common_exception_Unauthorized $e) {
499            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
500            return;
501        }
502
503        $this->signatureValidator->checkSignature(
504            $this->getRequestParameter('signature'),
505            $id
506        );
507
508        $response = [];
509
510        $class = $this->getClass($id);
511        $label = $this->getClassService()->createUniqueLabel($class);
512
513        $instance = $this->getClassService()->createInstance($class, $label);
514
515        if ($instance instanceof core_kernel_classes_Resource) {
516            $response = [
517                'success' => true,
518                'label' => $instance->getLabel(),
519                'uri'   => $instance->getUri()
520            ];
521        }
522
523        $this->returnJson($response);
524    }
525
526    /**
527     * Add a subclass to the currently selected class
528     * @requiresRight id WRITE
529     * @requiresRight classUri WRITE
530     *
531     * @throws Exception
532     * @throws common_exception_BadRequest
533     */
534    public function addSubClass()
535    {
536        if (!$this->isXmlHttpRequest()) {
537            throw new common_exception_BadRequest('wrong request mode');
538        }
539
540        $classId = $this->getRequestParameter('id');
541
542        $this->signatureValidator->checkSignature(
543            $this->getRequestParameter('signature'),
544            $classId
545        );
546
547        try {
548            $this->validateCsrf();
549        } catch (common_exception_Unauthorized $e) {
550            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
551            return;
552        }
553
554        $this->validateInstanceRoot($classId);
555
556        $parent = $this->getClass($classId);
557        $class = $this->getClassService()->createSubClass($parent);
558        if ($class instanceof core_kernel_classes_Class) {
559            $this->returnJson([
560                'success' => true,
561                'label' => $class->getLabel(),
562                'uri'   => tao_helpers_Uri::encode($class->getUri())
563            ]);
564        }
565    }
566
567    /**
568     * Add an instance of the selected class
569     * @throws common_exception_BadRequest
570     * @return void
571     */
572    public function addInstanceForm()
573    {
574        if (!$this->isXmlHttpRequest()) {
575            throw new common_exception_BadRequest('wrong request mode');
576        }
577
578        $class = $this->getCurrentClass();
579        $formContainer = new tao_actions_form_CreateInstance(
580            [$class],
581            [
582                 FormContainer::CSRF_PROTECTION_OPTION => true,
583                 FormContainer::ADDITIONAL_VALIDATORS => $this->getExtraValidationRules(),
584                 tao_actions_form_CreateInstance::EXCLUDED_PROPERTIES => $this->getExcludedProperties(),
585            ]
586        );
587
588        $addInstanceForm = $formContainer->getForm();
589
590        if ($addInstanceForm->isSubmited() && $addInstanceForm->isValid()) {
591            $properties = $addInstanceForm->getValues();
592            $instance = $this->createInstance([$class], $properties);
593
594            $this->setData('message', __('%s created', $instance->getLabel()));
595            $this->setData('reload', true);
596        }
597
598        $this->setData('formTitle', __('Create instance of ') . $class->getLabel());
599        $this->setData('myForm', $addInstanceForm->render());
600
601        $this->setView('form.tpl', 'tao');
602    }
603
604    /**
605     * creates the instance
606     *
607     * @param array $classes
608     * @param array $properties
609     * @return core_kernel_classes_Resource
610     */
611    protected function createInstance($classes, $properties)
612    {
613        $first = array_shift($classes);
614        $instance = $first->createInstanceWithProperties($properties);
615        foreach ($classes as $class) {
616            $instance = $this->getResource('');
617            $instance->setType($class);
618        }
619        return $instance;
620    }
621
622    public function editInstance()
623    {
624        $class = $this->getCurrentClass();
625        $instance = $this->getCurrentInstance();
626        $myFormContainer = new SignedFormInstance(
627            $class,
628            $instance,
629            [
630                FormContainer::CSRF_PROTECTION_OPTION => true,
631                FormContainer::ADDITIONAL_VALIDATORS => $this->getExtraValidationRules(),
632                tao_actions_form_Instance::EXCLUDED_PROPERTIES => $this->getExcludedProperties()
633            ]
634        );
635
636        $myForm = $myFormContainer->getForm();
637        if ($myForm->isSubmited() && $myForm->isValid()) {
638            $values = $myForm->getValues();
639            // save properties
640            $binder = new tao_models_classes_dataBinding_GenerisFormDataBinder($instance);
641            $binder->bind($values);
642            $message = __('Instance saved');
643
644            $this->setData('message', $message);
645            $this->setData('reload', true);
646        }
647
648        $this->setData('formTitle', __('Edit Instance'));
649        $this->setData('myForm', $myForm->render());
650        $this->setView('form.tpl', 'tao');
651    }
652
653    /**
654     * Duplicate the current instance
655     * render a JSON response
656     *
657     * @throws common_Exception
658     * @throws common_exception_BadRequest
659     * @throws common_exception_Error
660     * @throws tao_models_classes_MissingRequestParameterException
661     *
662     * @return void
663     * @requiresRight uri READ
664     * @requiresRight classUri WRITE
665     */
666    public function cloneInstance()
667    {
668        if (!$this->isXmlHttpRequest()) {
669            throw new common_exception_BadRequest('wrong request mode');
670        }
671
672        $uri = $this->getRequestParameter('uri');
673
674        $this->signatureValidator->checkSignature(
675            $this->getRequestParameter('signature'),
676            $uri
677        );
678
679        try {
680            $this->validateCsrf();
681        } catch (common_exception_Unauthorized $e) {
682            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
683            return;
684        }
685        $this->validateInstanceRoot($uri);
686
687        $clone = $this->getClassService()->cloneInstance($this->getCurrentInstance(), $this->getCurrentClass());
688        if ($clone !== null) {
689            $this->returnJson([
690                'success' => true,
691                'message' => __('Successfully cloned instance as %s', $clone->getLabel()),
692                'label' => $clone->getLabel(),
693                'uri'   => tao_helpers_Uri::encode($clone->getUri())
694            ]);
695        }
696    }
697
698    /**
699     * Copy a resource to a destination
700     *
701     * @requiresRight uri READ
702     */
703    public function copyInstance()
704    {
705        if (
706            $this->hasRequestParameter('destinationClassUri')
707            && $this->hasRequestParameter('uri')
708            && common_Utils::isUri($this->getRequestParameter('destinationClassUri'))
709        ) {
710            try {
711                $this->validateCsrf();
712            } catch (common_exception_Unauthorized $e) {
713                $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
714                return;
715            }
716            $this->validateInstanceRoot(
717                $this->getRequestParameter('uri')
718            );
719
720            $this->signatureValidator->checkSignature(
721                $this->getRequestParameter('signature'),
722                $this->getRequestParameter('uri')
723            );
724
725            $instance  = $this->getCurrentInstance();
726            $destinationClass = $this->getClass($this->getRequestParameter('destinationClassUri'));
727
728            if ($this->hasWriteAccess($destinationClass->getUri())) {
729                try {
730                    $result = $this->getResourceTransfer()->transfer(
731                        new ResourceTransferCommand(
732                            $instance->getUri(),
733                            $destinationClass->getUri(),
734                            $this->getRequestParameter('aclMode'),
735                            ResourceTransferCommand::TRANSFER_MODE_COPY
736                        )
737                    );
738
739                    $copy = $this->getResource($result->getDestination());
740
741                    return $this->returnJson(
742                        [
743                            'success'  => true,
744                            'data' => [
745                                'label' => $copy->getLabel(),
746                                'uri'   => $copy->getUri(),
747                            ]
748                        ]
749                    );
750                } catch (Throwable $exception) {
751                    $this->logError(
752                        sprintf(
753                            'Error copying instance %s to %s: %s',
754                            $instance->getUri(),
755                            $destinationClass->getUri(),
756                            $exception->getMessage() . ' - ' . $exception->getTraceAsString()
757                        )
758                    );
759
760                    return $this->returnJson(
761                        [
762                            'success'  => false,
763                            'errorCode' => 204,
764                            'errorMessage' =>  __("Unable to copy the resource")
765                        ],
766                        204
767                    );
768                }
769            }
770
771            return $this->returnJson(
772                [
773                    'success'  => false,
774                    'errorCode' => 401,
775                    'errorMessage' =>  __("Permission denied to write in the selected class")
776                ],
777                401
778            );
779        }
780
781        return $this->returnJson(
782            [
783                'success' => false,
784                'errorCode' => 412,
785                'errorMessage' => __('Missing Parameters')
786            ],
787            412
788        );
789    }
790
791    /**
792     * @requiresRight classUri READ
793     */
794    public function copyClass(): void
795    {
796        $parsedBody = $this->getPsrRequest()->getParsedBody();
797
798        if (empty($parsedBody['classUri']) || empty($parsedBody['uri'])) {
799            $this->returnJson(
800                [
801                    'success' => false,
802                    'errorCode' => 412,
803                    'errorMessage' => __('Missing Parameters'),
804                ],
805                412
806            );
807
808            return;
809        }
810
811        $this->validateInstanceRoot($parsedBody['uri']);
812        $this->signatureValidator->checkSignature($parsedBody['signature'] ?? null, $parsedBody['uri']);
813
814        $currentClass = $this->getClass($parsedBody['uri']);
815        $destinationClass = $this->getClass($parsedBody['classUri']);
816
817        if (!$this->hasWriteAccess($destinationClass->getUri())) {
818            $this->returnJson(
819                [
820                    'success' => false,
821                    'errorCode' => 401,
822                    'errorMessage' =>  __('Permission denied to write in the selected class'),
823                ],
824                401
825            );
826
827            return;
828        }
829
830        try {
831            $task = $this->getQueueDispatcher()->createTask(
832                new CopyClassTask(),
833                [
834                    CopyClassTask::PARAM_CLASS_URI => $currentClass->getUri(),
835                    CopyClassTask::PARAM_DESTINATION_CLASS_URI => $destinationClass->getUri(),
836                    CopyClassTask::PARAM_ACL_MODE => $parsedBody['aclMode'] ?? null,
837                ],
838                __(
839                    'Copying class "%s" to "%s"',
840                    $currentClass->getLabel(),
841                    $destinationClass->getLabel()
842                )
843            );
844
845            $this->returnTaskJson($task);
846        } catch (Throwable $exception) {
847            $this->logError(
848                sprintf(
849                    'Error copying class %s to %s: %s',
850                    $currentClass->getUri(),
851                    $destinationClass->getUri(),
852                    $exception->getMessage() . ' - ' . $exception->getTraceAsString()
853                )
854            );
855            $this->returnJson(
856                [
857                    'success' => false,
858                    'errorMessage' => __('Failed to copy class.'),
859                ]
860            );
861        }
862    }
863
864    /**
865     * Move an instance from a class to another
866     * @return void
867     * @requiresRight uri WRITE
868     * @requiresRight destinationClassUri WRITE
869     */
870    public function moveInstance()
871    {
872        if (!$this->hasRequestParameter('destinationClassUri') || !$this->hasRequestParameter('uri')) {
873            return $this->returnJson([]);
874        }
875
876        try {
877            $this->validateCsrf();
878        } catch (common_exception_Unauthorized $e) {
879            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
880
881            return;
882        }
883
884        $id = $this->getRequestParameter('uri');
885        $this->validateInstanceRoot($id);
886
887        $this->signatureValidator->checkSignature($this->getRequestParameter('signature'), $id);
888
889        $instance = $this->getResource($id);
890        $types = $instance->getTypes();
891        $class = reset($types);
892        $destinationUri = tao_helpers_Uri::decode($this->getRequestParameter('destinationClassUri'));
893        $this->validateDestinationClass($destinationUri, $class->getUri());
894        $destinationClass = $this->getClass($destinationUri);
895        $confirmed = $this->getRequestParameter('confirmed');
896
897        try {
898            if (!filter_var($confirmed, FILTER_VALIDATE_BOOLEAN)) {
899                $diff = $this->getClassService()->getPropertyDiff($class, $destinationClass);
900
901                if (!empty($diff)) {
902                    return $this->returnJson(
903                        [
904                            'status' => 'diff',
905                            'data' => $diff,
906                        ]
907                    );
908                }
909            }
910
911            $result = $this->getResourceTransfer()->transfer(
912                new ResourceTransferCommand(
913                    $instance->getUri(),
914                    $destinationUri,
915                    $this->getRequestParameter('aclMode'),
916                    ResourceTransferCommand::TRANSFER_MODE_MOVE
917                )
918            );
919
920            return $this->returnJson(
921                [
922                    'status' => true,
923                    'data' => [
924                        $result->getDestination() => [
925                            'success' => true,
926                            'message' => sprintf(
927                                'Instance "%s" has been successfully moved to "%s"',
928                                $result->getDestination(),
929                                $destinationUri
930                            ),
931                        ],
932                    ],
933                ]
934            );
935        } catch (Throwable $exception) {
936            $this->logError(
937                sprintf(
938                    'Error moving instance %s to %s: %s',
939                    $instance->getUri(),
940                    $destinationClass->getUri(),
941                    $exception->getMessage() . ' - ' . $exception->getTraceAsString()
942                )
943            );
944
945            return $this->returnJson(
946                [
947                    'status' => false,
948                ]
949            );
950        }
951    }
952
953    /**
954     * Move a single resource to another class
955     *
956     * @requiresRight uri WRITE
957     *
958     * @throws common_exception_Error
959     * @throws common_exception_MethodNotAllowed
960     */
961    public function moveResource()
962    {
963        try {
964            if (!$this->hasRequestParameter('uri')) {
965                throw new InvalidArgumentException('Resource uri must be specified.');
966            }
967
968            $data = $this->getRequestParameter('uri');
969            $id = $data['id'];
970            $destinationClassUri = $this->getRequestParameter('destinationClassUri');
971
972            try {
973                $this->validateCsrf();
974            } catch (common_exception_Unauthorized $e) {
975                $this->response = $this->getPsrResponse()->withStatus(
976                    403,
977                    __('Unable to process your request')
978                );
979
980                return;
981            }
982
983            $this->validateUri($id);
984            $this->validateInstanceRoot($id);
985            $this->signatureValidator->checkSignature($data['signature'], $id);
986            $this->validateMoveRequest();
987            $this->validateDestinationClass($destinationClassUri, $this->getResourceParentClass($id));
988
989            $result = $this->getResourceTransfer()->transfer(
990                new ResourceTransferCommand(
991                    $id,
992                    $destinationClassUri,
993                    $this->getRequestParameter('aclMode'),
994                    ResourceTransferCommand::TRANSFER_MODE_MOVE
995                )
996            );
997
998            return $this->returnJson(
999                [
1000                    'success' => true,
1001                    'data' => [
1002                        $result->getDestination() => [
1003                            'success' => true,
1004                            'message' => sprintf(
1005                                'Resource "%s" has been successfully moved to "%s"',
1006                                $id,
1007                                $destinationClassUri
1008                            )
1009                        ],
1010                    ],
1011                ]
1012            );
1013        } catch (InvalidArgumentException $e) {
1014            $this->returnJson(
1015                [
1016                    'success' => true,
1017                    'data' => [
1018                        'failed' => [
1019                            'success' => false,
1020                            'message' => $e->getMessage(),
1021                        ],
1022                    ],
1023                ],
1024                204
1025            );
1026        }
1027    }
1028
1029    /**
1030     * Move all specififed resources to the given destination root class
1031     *
1032     * @throws common_exception_Error
1033     * @throws common_exception_MethodNotAllowed
1034     * @requiresRight id WRITE
1035     */
1036    public function moveAll()
1037    {
1038        try {
1039            if (!$this->hasRequestParameter('ids')) {
1040                throw new InvalidArgumentException('Resource ids must be specified.');
1041            }
1042            $ids = [];
1043
1044            foreach ($this->getRequestParameter('ids') as $id) {
1045                $ids[] = $id['id'];
1046            }
1047
1048            $this->validateInstancesRoot($ids);
1049
1050            if (empty($ids)) {
1051                throw new InvalidArgumentException('No instances specified.');
1052            }
1053
1054            $this->signatureValidator->checkSignatures(
1055                $this->getRequestParameter('ids')
1056            );
1057
1058            $this->validateMoveRequest();
1059
1060            $response = $this->moveAllInstances($ids);
1061            $this->returnJson($response);
1062        } catch (InvalidArgumentException $e) {
1063            $this->returnJsonError($e->getMessage());
1064        }
1065    }
1066
1067    /**
1068     * Render the form to translate a Resource instance
1069     * @return void
1070     * @throws common_exception_Error
1071     * @throws tao_models_classes_MissingRequestParameterException
1072     * @requiresRight id WRITE
1073     */
1074    public function translateInstance()
1075    {
1076        $instance = $this->getCurrentInstance();
1077
1078        $formContainer = new tao_actions_form_Translate(
1079            $this->getCurrentClass(),
1080            $instance,
1081            [FormContainer::CSRF_PROTECTION_OPTION => true]
1082        );
1083        $myForm = $formContainer->getForm();
1084
1085        if ($this->hasRequestParameter('target_lang')) {
1086            $targetLang = $this->getRequestParameter('target_lang');
1087            $availableLanguages = tao_helpers_I18n::getAvailableLangsByUsage(
1088                $this->getResource(tao_models_classes_LanguageService::INSTANCE_LANGUAGE_USAGE_DATA)
1089            );
1090
1091            if (in_array($targetLang, $availableLanguages)) {
1092                $langElt = $myForm->getElement('translate_lang');
1093                $langElt->setValue($targetLang);
1094                $langElt->setAttribute('readonly', 'true');
1095
1096                $trData = $this->getClassService()->getTranslatedProperties($instance, $targetLang);
1097                foreach ($trData as $key => $value) {
1098                    $element = $myForm->getElement(tao_helpers_Uri::encode($key));
1099                    if ($element !== null) {
1100                        $element->setValue($value);
1101                    }
1102                }
1103            }
1104        }
1105
1106        if ($myForm->isSubmited() && $myForm->isValid()) {
1107            $values = $myForm->getValues();
1108            if (isset($values['translate_lang'])) {
1109                $datalang = $this->getSession()->getDataLanguage();
1110                $lang = $values['translate_lang'];
1111
1112                $translated = 0;
1113                foreach ($values as $key => $value) {
1114                    if (0 === strpos($key, 'http')) {
1115                        $value = trim($value);
1116                        $property = $this->getProperty($key);
1117                        if (empty($value)) {
1118                            if ($datalang !== $lang && $lang !== '') {
1119                                $instance->removePropertyValueByLg($property, $lang);
1120                            }
1121                        } elseif ($instance->editPropertyValueByLg($property, $value, $lang)) {
1122                            $translated++;
1123                        }
1124                    }
1125                }
1126                if ($translated > 0) {
1127                    $this->setData('message', __('Translation saved'));
1128                }
1129            }
1130        }
1131
1132        $this->setData('myForm', $myForm->render());
1133        $this->setData('formTitle', __('Translate'));
1134        $this->setView('form.tpl', 'tao');
1135    }
1136
1137    /**
1138     * load the translated data of an instance regarding the given lang
1139     *
1140     * @throws common_exception_BadRequest
1141     * @throws common_exception_Error
1142     * @throws tao_models_classes_MissingRequestParameterException
1143     *
1144     * @return void
1145     */
1146    public function getTranslatedData()
1147    {
1148        if (!$this->isXmlHttpRequest()) {
1149            throw new common_exception_BadRequest('wrong request mode');
1150        }
1151        $data = [];
1152        if ($this->hasRequestParameter('lang')) {
1153            $data = tao_helpers_Uri::encodeArray(
1154                $this->getClassService()->getTranslatedProperties(
1155                    $this->getCurrentInstance(),
1156                    $this->getRequestParameter('lang')
1157                ),
1158                tao_helpers_Uri::ENCODE_ARRAY_KEYS
1159            );
1160        }
1161        $this->returnJson($data);
1162    }
1163
1164    /**
1165     * delete an instance or a class
1166     * called via ajax
1167     *
1168     * @throws common_exception_BadRequest
1169     * @throws common_exception_MissingParameter
1170     *
1171     * @requiresRight id WRITE
1172     */
1173    public function delete()
1174    {
1175        if (!$this->isXmlHttpRequest()) {
1176            throw new common_exception_BadRequest('wrong request mode');
1177        }
1178
1179        if ($this->hasRequestParameter('uri')) {
1180            $this->forward(
1181                'deleteResource',
1182                null,
1183                null,
1184                (
1185                    [
1186                        'id' => tao_helpers_Uri::decode($this->getRequestParameter('uri'))
1187                    ]
1188                )
1189            );
1190        } elseif ($this->hasRequestParameter('classUri')) {
1191            $this->forward(
1192                'deleteClass',
1193                null,
1194                null,
1195                (
1196                    [
1197                        'id' => tao_helpers_Uri::decode($this->getRequestParameter('classUri'))
1198                    ]
1199                )
1200            );
1201        } else {
1202            throw new common_exception_MissingParameter();
1203        }
1204    }
1205
1206    /**
1207     * Generic resource deletion action
1208     *
1209     * @throws Exception
1210     * @throws common_exception_BadRequest
1211     * @requiresRight id WRITE
1212     */
1213    public function deleteResource()
1214    {
1215        if (!$this->isXmlHttpRequest() || !$this->hasRequestParameter('id')) {
1216            throw new common_exception_BadRequest('wrong request mode');
1217        }
1218
1219        $id = $this->getRequestParameter('id');
1220
1221        // Csrf token validation
1222        try {
1223            $this->validateCsrf();
1224        } catch (common_exception_Unauthorized $e) {
1225            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
1226            return;
1227        }
1228
1229        $this->validateInstanceRoot($id);
1230
1231        $this->signatureValidator->checkSignature(
1232            $this->getRequestParameter('signature'),
1233            $id
1234        );
1235
1236        $resource = $this->getResource($this->getRequestParameter('id'));
1237
1238        try {
1239            $this->getResourceDeleter()->delete($resource);
1240            $deleted = true;
1241            $message = __('Successfully deleted %s', $resource->getLabel());
1242        } catch (ResourceDeletionException $exception) {
1243            $deleted = false;
1244            $message = $exception->getUserMessage();
1245        }
1246
1247        return $this->returnJson([
1248            'success' => $deleted,
1249            'message' => $message,
1250            'deleted' => $deleted,
1251        ]);
1252    }
1253
1254    /**
1255     * Generic class deletion action
1256     *
1257     * @throws Exception
1258     * @throws common_exception_BadRequest
1259     * @requiresRight id WRITE
1260     */
1261    public function deleteClass()
1262    {
1263        if (!$this->isXmlHttpRequest() || !$this->hasRequestParameter('id')) {
1264            throw new common_exception_BadRequest('wrong request mode');
1265        }
1266
1267        $id = $this->getRequestParameter('id');
1268
1269        // Csrf token validation
1270        try {
1271            $this->validateCsrf();
1272        } catch (common_exception_Unauthorized $e) {
1273            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
1274            return;
1275        }
1276
1277        $this->validateInstanceRoot($id);
1278
1279        $this->signatureValidator->checkSignature(
1280            $this->getRequestParameter('signature'),
1281            $id
1282        );
1283
1284        $class = $this->getClass($id);
1285        $label = $class->getLabel();
1286        $classDeleter = $this->getClassDeleter();
1287
1288        try {
1289            $classDeleter->delete($class);
1290            $success = true;
1291            $deleted = true;
1292            $message = __('%s has been deleted', $label);
1293        } catch (PartialClassDeletionException | ClassDeletionException $exception) {
1294            $success = $exception instanceof PartialClassDeletionException;
1295            $deleted = false;
1296
1297            $message = $exception->getUserMessage();
1298        }
1299
1300        $this->returnJson([
1301            'success' => $success,
1302            'message' => $message,
1303            'deleted' => $deleted,
1304        ]);
1305    }
1306
1307    /**
1308     * Delete all given resources
1309     *
1310     * @requiresRight ids WRITE
1311     *
1312     * @throws Exception
1313     * @throws common_exception_BadRequest
1314     */
1315    public function deleteAll()
1316    {
1317        $response = [
1318            'success' => true,
1319            'deleted' => []
1320        ];
1321        if (!$this->isXmlHttpRequest()) {
1322            throw new common_exception_BadRequest('wrong request mode');
1323        }
1324
1325        // Csrf token validation
1326        try {
1327            $this->validateCsrf();
1328        } catch (common_exception_Unauthorized $e) {
1329            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
1330            return;
1331        }
1332
1333        $ids = [];
1334
1335        foreach ($this->getRequestParameter('ids') as $id) {
1336            $ids[] = $id['id'];
1337        }
1338
1339        $this->signatureValidator->checkSignatures($this->getRequestParameter('ids'));
1340
1341        $this->validateInstancesRoot($ids);
1342
1343        foreach ($ids as $id) {
1344            $deleted = false;
1345            $deletedResourceLabel = '';
1346            try {
1347                if ($this->hasWriteAccess($id)) {
1348                    $resource = new \core_kernel_classes_Resource($id);
1349                    if ($resource->isClass()) {
1350                        $class = new core_kernel_classes_Class($id);
1351                        $deletedResourceLabel = $class->getLabel();
1352                        $deleted = $this->getClassService()->deleteClass($class);
1353                    } else {
1354                        $deletedResourceLabel = $resource->getLabel();
1355                        $deleted = $this->getClassService()->deleteResource($resource);
1356                    }
1357                }
1358            } catch (\common_Exception $ce) {
1359                \common_Logger::w('Unable to remove resource ' . $id . ' : ' . $ce->getMessage());
1360            }
1361            if ($deleted) {
1362                $response['deleted'][] = $id;
1363            }
1364            $response['message'] = __('Successfully deleted %s', $deletedResourceLabel);
1365        }
1366
1367        return $this->returnJson($response);
1368    }
1369
1370    /**
1371     * Copy of \tao_actions_PropertiesAuthoring::addClassProperty to split access between extensions
1372     *
1373     * @requiresRight id WRITE
1374     */
1375    public function addClassProperty(AddClassPropertyFormFactory $addClassPropertyFormFactory): void
1376    {
1377        if (!$this->isXmlHttpRequest()) {
1378            throw new common_exception_BadRequest('wrong request mode');
1379        }
1380
1381        $myForm = $addClassPropertyFormFactory->add(
1382            $this->getPsrRequest(),
1383            $this->hasWriteAccessToAction(__FUNCTION__)
1384        );
1385
1386        $this->setData('data', $myForm->renderElements());
1387        $this->setView('blank.tpl', 'tao');
1388    }
1389
1390    /**
1391     * Copy of \tao_actions_PropertiesAuthoring::removeClassProperty to split access between extensions
1392     *
1393     * @requiresRight classUri WRITE
1394     * @throws common_Exception
1395     */
1396    public function removeClassProperty(RemoveClassPropertyService $removeClassPropertyService): void
1397    {
1398        if (!$this->isXmlHttpRequest()) {
1399            throw new common_exception_BadRequest('wrong request mode');
1400        }
1401
1402        $success = $removeClassPropertyService->remove($this->getPsrRequest());
1403
1404        if ($success) {
1405            $this->returnJson(['success' => true]);
1406        } else {
1407            $this->returnError(__('Unable to remove the property.'));
1408        }
1409    }
1410
1411    /**
1412     * Test whenever the current user has "WRITE" access to the specified id
1413     *
1414     * @param string $resourceId
1415     * @return boolean
1416     */
1417    protected function hasWriteAccess($resourceId)
1418    {
1419        /** @var PermissionChecker $permissionChecker */
1420        $permissionChecker = $this->getServiceLocator()->get(PermissionChecker::class);
1421
1422        return $permissionChecker->hasWriteAccess($resourceId);
1423    }
1424
1425    /**
1426     * Validate request with all required parameters
1427     *
1428     * @throws common_exception_Error
1429     * @throws common_exception_MethodNotAllowed If it is not POST method
1430     * @throws InvalidArgumentException If parameters are not correct
1431     */
1432    protected function validateMoveRequest()
1433    {
1434        if (!$this->isRequestPost()) {
1435            throw new common_exception_MethodNotAllowed('Only POST method is allowed to move instances.');
1436        }
1437
1438        if (!$this->hasRequestParameter('destinationClassUri')) {
1439            throw new InvalidArgumentException('Destination class must be specified');
1440        }
1441
1442        $destinationClass = new core_kernel_classes_Class($this->getRequestParameter('destinationClassUri'));
1443        if (!$destinationClass->isClass()) {
1444            throw new InvalidArgumentException('Destination class must be a valid class');
1445        }
1446    }
1447
1448    /**
1449     * Move instances to another class
1450     *
1451     * {
1452     *   "destinationClassUri": "http://test.it",
1453     *   "ids": [
1454     *     "http://resource1",
1455     *     "http://resource2",
1456     *     "http://class1",
1457     *     "http://class2"
1458     *   ]
1459     * }
1460     * @requiresRight destinationClassUri WRITE
1461     * @params array $ids The list of instance uris to move
1462     *
1463     * @throws common_exception_Error
1464     */
1465    protected function moveAllInstances(array $ids)
1466    {
1467        $rootClass = $this->getClassService()->getRootClass();
1468
1469        if (in_array($rootClass->getUri(), $ids)) {
1470            throw new InvalidArgumentException(sprintf('Root class "%s" cannot be moved', $rootClass->getUri()));
1471        }
1472
1473        $destinationClass = new core_kernel_classes_Class($this->getRequestParameter('destinationClassUri'));
1474
1475        if (!$destinationClass->isSubClassOf($rootClass) && $destinationClass->getUri() != $rootClass->getUri()) {
1476            throw new InvalidArgumentException(
1477                sprintf(
1478                    'Instance "%s" cannot be moved to another root class',
1479                    $destinationClass->getUri()
1480                )
1481            );
1482        }
1483
1484        [$statuses, $instances, $classes] = $this->getInstancesList($ids);
1485        $movableInstances = $this->getInstancesToMove($classes, $instances, $statuses);
1486
1487        $statuses = $this->move($destinationClass, $movableInstances, $statuses);
1488
1489        return [
1490            'success' => true,
1491            'data' => $statuses
1492        ];
1493    }
1494
1495    /**
1496     * Gets list of existing instances/classes
1497     *
1498     * @param array $ids list of ids asked to be moved
1499     * @return array
1500     */
1501    private function getInstancesList(array $ids)
1502    {
1503        $statuses = $instances = $classes = [];
1504
1505        foreach ($ids as $key => $instance) {
1506            $instance = $this->getResource($instance);
1507            if ($instance->isClass()) {
1508                $instance = $this->getClass($instance);
1509                $classes[] = $instance;
1510            } elseif (!$instance->exists()) {
1511                $statuses[$instance->getUri()] = [
1512                    'success' => false,
1513                    'message' => sprintf('Instance "%s" does not exist', $instance->getUri()),
1514                ];
1515                break;
1516            }
1517            $instances[$key] = $instance;
1518        }
1519
1520        return [
1521            $statuses,
1522            $instances,
1523            $classes
1524        ];
1525    }
1526
1527    /**
1528     * Get movable instances from the list of instances
1529     *
1530     * @param array $classes
1531     * @param array $instances
1532     *
1533     * @return array
1534     */
1535    private function getInstancesToMove(array $classes = [], array $instances = [], array &$statuses = [])
1536    {
1537        $movableInstances = [];
1538
1539        // Check if a class belong to class to move
1540        /** @var core_kernel_classes_Resource|core_kernel_classes_Class $instance */
1541        foreach ($instances as $instance) {
1542            $isValid = true;
1543            foreach ($classes as $class) {
1544                if ($instance instanceof core_kernel_classes_Class) {
1545                    //Disallow moving a class to $class. True only for classes which are already subclasses of $class
1546                    if ($class->getUri() != $instance->getUri() && $instance->isSubClassOf($class)) {
1547                        $statuses[$instance->getUri()] = [
1548                            'success' => false,
1549                            'message' => sprintf(
1550                                'Instance "%s" cannot be moved to class to move "%s"',
1551                                $instance->getUri(),
1552                                $class->getUri()
1553                            ),
1554                        ];
1555                        $isValid = false;
1556                        break;
1557                    }
1558                } else {
1559                    //Disallow moving instances to $class. True only for instances which already belongs to $class
1560                    if ($instance->isInstanceOf($class)) {
1561                        $statuses[$instance->getUri()] = [
1562                            'success' => false,
1563                            'message' => sprintf(
1564                                'Instance "%s" cannot be moved to class to move "%s"',
1565                                $instance->getUri(),
1566                                $class->getUri()
1567                            ),
1568                        ];
1569                        $isValid = false;
1570                        break;
1571                    }
1572                }
1573            }
1574            if ($isValid) {
1575                $movableInstances[$instance->getUri()] = $instance;
1576            }
1577        }
1578
1579        return $movableInstances;
1580    }
1581
1582    /**
1583     * Move movableInstances to the destinationClass
1584     *
1585     * @param core_kernel_classes_Class $destinationClass class to move to
1586     * @param array $movableInstances list of instances available to move
1587     * @param array $statuses list of statuses for instances asked to be moved
1588     *
1589     * @return array $statuses updated list of statuses
1590     */
1591    private function move(
1592        core_kernel_classes_Class $destinationClass,
1593        array $movableInstances = [],
1594        array $statuses = []
1595    ) {
1596        /** @var core_kernel_classes_Resource $movableInstance */
1597        foreach ($movableInstances as $movableInstance) {
1598            $statuses[$movableInstance->getUri()] = [
1599                'success' => $success = $this->getClassService()->changeClass($movableInstance, $destinationClass),
1600            ];
1601            if ($success === true) {
1602                $statuses[$movableInstance->getUri()]['message'] = sprintf(
1603                    'Instance "%s" has been successfully moved to "%s"',
1604                    $movableInstance->getUri(),
1605                    $destinationClass->getUri()
1606                );
1607
1608                $this->getEventManager()->trigger(new ResourceMovedEvent($movableInstance, $destinationClass));
1609            } else {
1610                $statuses[$movableInstance->getUri()]['message'] = sprintf(
1611                    'An error has occurred while persisting instance "%s"',
1612                    $movableInstance->getUri()
1613                );
1614            }
1615        }
1616
1617        return $statuses;
1618    }
1619
1620    /**
1621     * Return a formatted error message with code 406
1622     *
1623     * @param $message
1624     * @param int $httpStatusCode
1625     */
1626    protected function returnJsonError($message, $httpStatusCode = 406)
1627    {
1628        $response = [
1629            'success'  => false,
1630            'errorCode' => $httpStatusCode,
1631            'errorMessage' =>  $message
1632        ];
1633        $this->returnJson($response, 406);
1634    }
1635
1636    protected function getExtraValidationRules(): array
1637    {
1638        return [];
1639    }
1640
1641    protected function getExcludedProperties(): array
1642    {
1643        return [];
1644    }
1645
1646    /**
1647     * @return ActionService
1648     */
1649    private function getActionService()
1650    {
1651        return $this->getServiceLocator()->get(ActionService::SERVICE_ID);
1652    }
1653
1654    /**
1655     * Get the resource service
1656     * @return ResourceService
1657     */
1658    protected function getResourceService()
1659    {
1660        return $this->getServiceLocator()->get(ResourceService::SERVICE_ID);
1661    }
1662
1663    /**
1664     * @return SignatureGenerator
1665     */
1666    private function getSignatureGenerator()
1667    {
1668        return $this->getServiceLocator()->get(SignatureGenerator::SERVICE_ID);
1669    }
1670
1671    /**
1672     * @return string
1673     *
1674     * @throws InconsistencyConfigException
1675     */
1676    private function createFormSignature()
1677    {
1678        return $this->getSignatureGenerator()->generate(
1679            tao_helpers_Uri::encode($this->getRequestParameter('classUri'))
1680        );
1681    }
1682
1683    /**
1684     * @param $destinationUri
1685     * @param $currentClassUri
1686     */
1687    private function validateDestinationClass($destinationUri, $currentClassUri)
1688    {
1689        $destinationClass = $this->getClass($destinationUri);
1690        if (empty($destinationUri) || $destinationUri === $currentClassUri || !$destinationClass->exists()) {
1691            throw new InvalidArgumentException('The selected class already contains the resource');
1692        }
1693    }
1694
1695    private function getEventManager(): EventManager
1696    {
1697        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
1698    }
1699
1700    private function getClassDeleter(): ClassDeleterInterface
1701    {
1702        return $this->getPsrContainer()->get(ClassDeleter::class);
1703    }
1704
1705    private function getResourceDeleter(): ResourceDeleterInterface
1706    {
1707        return $this->getPsrContainer()->get(ResourceDeleter::class);
1708    }
1709
1710    private function getQueueDispatcher(): QueueDispatcherInterface
1711    {
1712        return $this->getPsrContainer()->get(QueueDispatcherInterface::SERVICE_ID);
1713    }
1714
1715    private function getResourceTransfer(): ResourceTransferInterface
1716    {
1717        return $this->getPsrContainer()->get(ResourceTransferProxy::class);
1718    }
1719
1720    private function getResourceParentClass(string $id): string
1721    {
1722        if ($this->getResource($id)->isClass()) {
1723            $directParent = $this->getResource($id)->getParentClassesIds();
1724            return reset($directParent);
1725        }
1726
1727        $types = $this->getResource($id)->getTypes();
1728
1729        if (count($types) !== 1) {
1730            throw new InvalidArgumentException('Resource has no class or resource types are ambiguous');
1731        }
1732
1733        /** @var core_kernel_classes_Class $type */
1734        $type = reset($types);
1735
1736        if (!($type instanceof core_kernel_classes_Class) && !$type->isClass()) {
1737            throw new InvalidArgumentException('Type is not a class');
1738        }
1739
1740        return $type->getUri();
1741    }
1742}