vendor/pimcore/pimcore/bundles/AdminBundle/Controller/Searchadmin/SearchController.php line 447

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Bundle\AdminBundle\Controller\Searchadmin;
  15. use Pimcore\Bundle\AdminBundle\Controller\AdminController;
  16. use Pimcore\Bundle\AdminBundle\Controller\Traits\AdminStyleTrait;
  17. use Pimcore\Bundle\AdminBundle\Helper\GridHelperService;
  18. use Pimcore\Config;
  19. use Pimcore\Event\Admin\ElementAdminStyleEvent;
  20. use Pimcore\Event\AdminEvents;
  21. use Pimcore\Model\Asset;
  22. use Pimcore\Model\DataObject;
  23. use Pimcore\Model\Document;
  24. use Pimcore\Model\Element;
  25. use Pimcore\Model\Search\Backend\Data;
  26. use Symfony\Component\EventDispatcher\GenericEvent;
  27. use Symfony\Component\HttpFoundation\JsonResponse;
  28. use Symfony\Component\HttpFoundation\Request;
  29. use Symfony\Component\Routing\Annotation\Route;
  30. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  31. /**
  32.  * @Route("/search")
  33.  *
  34.  * @internal
  35.  */
  36. class SearchController extends AdminController
  37. {
  38.     use AdminStyleTrait;
  39.     /**
  40.      * @Route("/find", name="pimcore_admin_searchadmin_search_find", methods={"GET", "POST"})
  41.      *
  42.      * @param Request $request
  43.      *
  44.      * @return JsonResponse
  45.      *
  46.      * @todo: $conditionTypeParts could be undefined
  47.      * @todo: $conditionSubtypeParts could be undefined
  48.      * @todo: $conditionClassnameParts could be undefined
  49.      * @todo: $data could be undefined
  50.      */
  51.     public function findAction(Request $requestEventDispatcherInterface $eventDispatcherGridHelperService $gridHelperService)
  52.     {
  53.         $allParams array_merge($request->request->all(), $request->query->all());
  54.         $requestedLanguage $allParams['language'] ?? null;
  55.         if ($requestedLanguage) {
  56.             if ($requestedLanguage != 'default') {
  57.                 $request->setLocale($requestedLanguage);
  58.             }
  59.         }
  60.         $filterPrepareEvent = new GenericEvent($this, [
  61.             'requestParams' => $allParams,
  62.         ]);
  63.         $eventDispatcher->dispatch($filterPrepareEventAdminEvents::SEARCH_LIST_BEFORE_FILTER_PREPARE);
  64.         $allParams $filterPrepareEvent->getArgument('requestParams');
  65.         $query $this->filterQueryParam($allParams['query'] ?? '');
  66.         $types explode(','$allParams['type'] ?? '');
  67.         $subtypes explode(','$allParams['subtype'] ?? '');
  68.         $classnames explode(','$allParams['class'] ?? '');
  69.         $offset = (int)$allParams['start'];
  70.         $limit = (int)$allParams['limit'];
  71.         $offset $offset $offset 0;
  72.         $limit $limit $limit 50;
  73.         $searcherList = new Data\Listing();
  74.         $conditionParts = [];
  75.         $db \Pimcore\Db::get();
  76.         $conditionParts[] = $this->getPermittedPaths($types);
  77.         $queryCondition '';
  78.         if (!empty($query)) {
  79.             $queryCondition '( MATCH (`data`,`properties`) AGAINST (' $db->quote($query) . ' IN BOOLEAN MODE) )';
  80.             // the following should be done with an exact-search now "ID", because the Element-ID is now in the fulltext index
  81.             // if the query is numeric the user might want to search by id
  82.             //if(is_numeric($query)) {
  83.             //$queryCondition = "(" . $queryCondition . " OR id = " . $db->quote($query) ." )";
  84.             //}
  85.             $conditionParts[] = $queryCondition;
  86.         }
  87.         //For objects - handling of bricks
  88.         $fields = [];
  89.         $bricks = [];
  90.         if (!empty($allParams['fields'])) {
  91.             $fields $allParams['fields'];
  92.             foreach ($fields as $f) {
  93.                 $parts explode('~'$f);
  94.                 if (substr($f01) == '~') {
  95.                     //                    $type = $parts[1];
  96. //                    $field = $parts[2];
  97. //                    $keyid = $parts[3];
  98.                     // key value, ignore for now
  99.                 } elseif (count($parts) > 1) {
  100.                     $bricks[$parts[0]] = $parts[0];
  101.                 }
  102.             }
  103.         }
  104.         // filtering for objects
  105.         if (!empty($allParams['filter']) && !empty($allParams['class'])) {
  106.             $class DataObject\ClassDefinition::getByName($allParams['class']);
  107.             // add Localized Fields filtering
  108.             $params $this->decodeJson($allParams['filter']);
  109.             $unlocalizedFieldsFilters = [];
  110.             $localizedFieldsFilters = [];
  111.             foreach ($params as $paramConditionObject) {
  112.                 //this loop divides filter parameters to localized and unlocalized groups
  113.                 $definitionExists in_array('o_' $paramConditionObject['property'], DataObject\Service::getSystemFields())
  114.                     || $class->getFieldDefinition($paramConditionObject['property']);
  115.                 if ($definitionExists) { //TODO: for sure, we can add additional condition like getLocalizedFieldDefinition()->getFieldDefiniton(...
  116.                     $unlocalizedFieldsFilters[] = $paramConditionObject;
  117.                 } else {
  118.                     $localizedFieldsFilters[] = $paramConditionObject;
  119.                 }
  120.             }
  121.             //get filter condition only when filters array is not empty
  122.             //string statements for divided filters
  123.             $conditionFilters count($unlocalizedFieldsFilters)
  124.                 ? $gridHelperService->getFilterCondition($this->encodeJson($unlocalizedFieldsFilters), $class)
  125.                 : null;
  126.             $localizedConditionFilters count($localizedFieldsFilters)
  127.                 ? $gridHelperService->getFilterCondition($this->encodeJson($localizedFieldsFilters), $class)
  128.                 : null;
  129.             $join '';
  130.             $localizedJoin '';
  131.             foreach ($bricks as $ob) {
  132.                 $join .= ' LEFT JOIN object_brick_query_' $ob '_' $class->getId();
  133.                 $join .= ' `' $ob '`';
  134.                 if ($localizedConditionFilters) {
  135.                     $localizedJoin $join ' ON `' $ob '`.o_id = `object_localized_data_' $class->getId() . '`.ooo_id';
  136.                 }
  137.                 $join .= ' ON `' $ob '`.o_id = `object_' $class->getId() . '`.o_id';
  138.             }
  139.             if (null !== $conditionFilters) {
  140.                 //add condition query for non localised fields
  141.                 $conditionParts[] = '( id IN (SELECT `object_' $class->getId() . '`.o_id FROM object_' $class->getId()
  142.                     . $join ' WHERE ' $conditionFilters ') )';
  143.             }
  144.             if (null !== $localizedConditionFilters) {
  145.                 //add condition query for localised fields
  146.                 $conditionParts[] = '( id IN (SELECT `object_localized_data_' $class->getId()
  147.                     . '`.ooo_id FROM object_localized_data_' $class->getId() . $localizedJoin ' WHERE '
  148.                     $localizedConditionFilters ' GROUP BY ooo_id ' ') )';
  149.             }
  150.         }
  151.         if (is_array($types) && !empty($types[0])) {
  152.             $conditionTypeParts = [];
  153.             foreach ($types as $type) {
  154.                 $conditionTypeParts[] = $db->quote($type);
  155.             }
  156.             if (in_array('folder'$subtypes)) {
  157.                 $conditionTypeParts[] = $db->quote('folder');
  158.             }
  159.             $conditionParts[] = '( maintype IN (' implode(','$conditionTypeParts) . ') )';
  160.         }
  161.         if (is_array($subtypes) && !empty($subtypes[0])) {
  162.             $conditionSubtypeParts = [];
  163.             foreach ($subtypes as $subtype) {
  164.                 $conditionSubtypeParts[] = $db->quote($subtype);
  165.             }
  166.             $conditionParts[] = '( type IN (' implode(','$conditionSubtypeParts) . ') )';
  167.         }
  168.         if (is_array($classnames) && !empty($classnames[0])) {
  169.             if (in_array('folder'$subtypes)) {
  170.                 $classnames[] = 'folder';
  171.             }
  172.             $conditionClassnameParts = [];
  173.             foreach ($classnames as $classname) {
  174.                 $conditionClassnameParts[] = $db->quote($classname);
  175.             }
  176.             $conditionParts[] = '( subtype IN (' implode(','$conditionClassnameParts) . ') )';
  177.         }
  178.         //filtering for tags
  179.         if (!empty($allParams['tagIds'])) {
  180.             $tagIds $allParams['tagIds'];
  181.             $tagsTypeCondition '';
  182.             if (is_array($types) && !empty($types[0])) {
  183.                 $tagsTypeCondition 'ctype IN (\'' implode('\',\''$types) . '\') AND';
  184.             } elseif (!is_array($types)) {
  185.                 $tagsTypeCondition 'ctype = ' $db->quote($types) . ' AND ';
  186.             }
  187.             foreach ($tagIds as $tagId) {
  188.                 if (($allParams['considerChildTags'] ?? 'false') === 'true') {
  189.                     $tag Element\Tag::getById($tagId);
  190.                     if ($tag) {
  191.                         $tagPath $tag->getFullIdPath();
  192.                         $conditionParts[] = 'id IN (SELECT cId FROM tags_assignment INNER JOIN tags ON tags.id = tags_assignment.tagid WHERE '.$tagsTypeCondition.' (id = ' .(int)$tagId' OR idPath LIKE ' $db->quote($db->escapeLike($tagPath) . '%') . '))';
  193.                     }
  194.                 } else {
  195.                     $conditionParts[] = 'id IN (SELECT cId FROM tags_assignment WHERE '.$tagsTypeCondition.' tagid = ' .(int)$tagId')';
  196.                 }
  197.             }
  198.         }
  199.         if (count($conditionParts) > 0) {
  200.             $condition implode(' AND '$conditionParts);
  201.             $searcherList->setCondition($condition);
  202.         }
  203.         $searcherList->setOffset($offset);
  204.         $searcherList->setLimit($limit);
  205.         $searcherList->setOrderKey($queryConditionfalse);
  206.         $searcherList->setOrder('DESC');
  207.         $sortingSettings \Pimcore\Bundle\AdminBundle\Helper\QueryParams::extractSortingSettings($allParams);
  208.         if ($sortingSettings['orderKey']) {
  209.             // we need a special mapping for classname as this is stored in subtype column
  210.             $sortMapping = [
  211.                 'classname' => 'subtype',
  212.             ];
  213.             $sort $sortingSettings['orderKey'];
  214.             if (array_key_exists($sortingSettings['orderKey'], $sortMapping)) {
  215.                 $sort $sortMapping[$sortingSettings['orderKey']];
  216.             }
  217.             $searcherList->setOrderKey($sort);
  218.         }
  219.         if ($sortingSettings['order']) {
  220.             $searcherList->setOrder($sortingSettings['order']);
  221.         }
  222.         $beforeListLoadEvent = new GenericEvent($this, [
  223.             'list' => $searcherList,
  224.             'context' => $allParams,
  225.         ]);
  226.         $eventDispatcher->dispatch($beforeListLoadEventAdminEvents::SEARCH_LIST_BEFORE_LIST_LOAD);
  227.         /** @var Data\Listing $searcherList */
  228.         $searcherList $beforeListLoadEvent->getArgument('list');
  229.         if (in_array('asset'$types)) {
  230.             // Global asset list event (same than the SEARCH_LIST_BEFORE_LIST_LOAD event, but this last one is global for search, list, tree)
  231.             $beforeListLoadEvent = new GenericEvent($this, [
  232.                 'list' => $searcherList,
  233.                 'context' => $allParams,
  234.             ]);
  235.             $eventDispatcher->dispatch($beforeListLoadEventAdminEvents::ASSET_LIST_BEFORE_LIST_LOAD);
  236.             /** @var Data\Listing $searcherList */
  237.             $searcherList $beforeListLoadEvent->getArgument('list');
  238.         }
  239.         if (in_array('document'$types)) {
  240.             // Global document list event (same than the SEARCH_LIST_BEFORE_LIST_LOAD event, but this last one is global for search, list, tree)
  241.             $beforeListLoadEvent = new GenericEvent($this, [
  242.                 'list' => $searcherList,
  243.                 'context' => $allParams,
  244.             ]);
  245.             $eventDispatcher->dispatch($beforeListLoadEventAdminEvents::DOCUMENT_LIST_BEFORE_LIST_LOAD);
  246.             /** @var Data\Listing $searcherList */
  247.             $searcherList $beforeListLoadEvent->getArgument('list');
  248.         }
  249.         if (in_array('object'$types)) {
  250.             // Global object list event (same than the SEARCH_LIST_BEFORE_LIST_LOAD event, but this last one is global for search, list, tree)
  251.             $beforeListLoadEvent = new GenericEvent($this, [
  252.                 'list' => $searcherList,
  253.                 'context' => $allParams,
  254.             ]);
  255.             $eventDispatcher->dispatch($beforeListLoadEventAdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD);
  256.             /** @var Data\Listing $searcherList */
  257.             $searcherList $beforeListLoadEvent->getArgument('list');
  258.         }
  259.         $hits $searcherList->load();
  260.         $elements = [];
  261.         foreach ($hits as $hit) {
  262.             $element Element\Service::getElementById($hit->getId()->getType(), $hit->getId()->getId());
  263.             if ($element->isAllowed('list')) {
  264.                 $data null;
  265.                 if ($element instanceof DataObject\AbstractObject) {
  266.                     $data DataObject\Service::gridObjectData($element$fields);
  267.                 } elseif ($element instanceof Document) {
  268.                     $data Document\Service::gridDocumentData($element);
  269.                 } elseif ($element instanceof Asset) {
  270.                     $data Asset\Service::gridAssetData($element);
  271.                 }
  272.                 if ($data) {
  273.                     $elements[] = $data;
  274.                 }
  275.             } else {
  276.                 //TODO: any message that view is blocked?
  277.                 //$data = Element\Service::gridElementData($element);
  278.             }
  279.         }
  280.         // only get the real total-count when the limit parameter is given otherwise use the default limit
  281.         if ($allParams['limit']) {
  282.             $totalMatches $searcherList->getTotalCount();
  283.         } else {
  284.             $totalMatches count($elements);
  285.         }
  286.         $result = ['data' => $elements'success' => true'total' => $totalMatches];
  287.         $afterListLoadEvent = new GenericEvent($this, [
  288.             'list' => $result,
  289.             'context' => $allParams,
  290.         ]);
  291.         $eventDispatcher->dispatch($afterListLoadEventAdminEvents::SEARCH_LIST_AFTER_LIST_LOAD);
  292.         $result $afterListLoadEvent->getArgument('list');
  293.         return $this->adminJson($result);
  294.     }
  295.     /**
  296.      * @internal
  297.      *
  298.      * @param array $types
  299.      *
  300.      * @return string
  301.      */
  302.     protected function getPermittedPaths($types = ['asset''document''object'])
  303.     {
  304.         $user $this->getAdminUser();
  305.         $db \Pimcore\Db::get();
  306.         $allowedTypes = [];
  307.         foreach ($types as $type) {
  308.             if ($user->isAllowed($type 's')) { //the permissions are just plural
  309.                 $elementPaths Element\Service::findForbiddenPaths($type$user);
  310.                 $forbiddenPathSql = [];
  311.                 $allowedPathSql = [];
  312.                 foreach ($elementPaths['forbidden'] as $forbiddenPath => $allowedPaths) {
  313.                     $exceptions '';
  314.                     $folderSuffix '';
  315.                     if ($allowedPaths) {
  316.                         $exceptionsConcat implode("%' OR fullpath LIKE '"$allowedPaths);
  317.                         $exceptions " OR (fullpath LIKE '" $exceptionsConcat "%')";
  318.                         $folderSuffix '/'//if allowed children are found, the current folder is listable but its content is still blocked, can easily done by adding a trailing slash
  319.                     }
  320.                     $forbiddenPathSql[] = ' (fullpath NOT LIKE ' $db->quote($forbiddenPath $folderSuffix '%') . $exceptions ') ';
  321.                 }
  322.                 foreach ($elementPaths['allowed'] as $allowedPaths) {
  323.                     $allowedPathSql[] = ' fullpath LIKE ' $db->quote($allowedPaths  '%');
  324.                 }
  325.                 // this is to avoid query error when implode is empty.
  326.                 // the result would be like `(maintype = type AND ((path1 OR path2) AND (not_path3 AND not_path4)))`
  327.                 $forbiddenAndAllowedSql '(maintype = \'' $type '\'';
  328.                 if ($allowedPathSql || $forbiddenPathSql) {
  329.                     $forbiddenAndAllowedSql .= ' AND (';
  330.                     $forbiddenAndAllowedSql .= $allowedPathSql '( ' implode(' OR '$allowedPathSql) . ' )' '';
  331.                     if ($forbiddenPathSql) {
  332.                         //if $allowedPathSql "implosion" is present, we need `AND` in between
  333.                         $forbiddenAndAllowedSql .= $allowedPathSql ' AND ' '';
  334.                         $forbiddenAndAllowedSql .= implode(' AND '$forbiddenPathSql);
  335.                     }
  336.                     $forbiddenAndAllowedSql .= ' )';
  337.                 }
  338.                 $forbiddenAndAllowedSql.= ' )';
  339.                 $allowedTypes[] = $forbiddenAndAllowedSql;
  340.             }
  341.         }
  342.         //if allowedTypes is still empty after getting the workspaces, it means that there are no any master permissions set
  343.         // by setting a `false` condition in the query makes sure that nothing would be displayed.
  344.         if (!$allowedTypes) {
  345.             $allowedTypes = ['false'];
  346.         }
  347.         return '('.implode(' OR '$allowedTypes) .')';
  348.     }
  349.     /**
  350.      * @param string $query
  351.      *
  352.      * @return string
  353.      */
  354.     protected function filterQueryParam(string $query)
  355.     {
  356.         if ($query == '*') {
  357.             $query '';
  358.         }
  359.         $query str_replace('&quot;''"'$query);
  360.         $query str_replace('%''*'$query);
  361.         $query str_replace('@''#'$query);
  362.         $query preg_replace("@([^ ])\-@"'$1 '$query);
  363.         $query str_replace(['<''>''('')''~'], ' '$query);
  364.         // it is not allowed to have * behind another *
  365.         $query preg_replace('#[*]+#''*'$query);
  366.         // no boolean operators at the end of the query
  367.         $query rtrim($query'+- ');
  368.         return $query;
  369.     }
  370.     /**
  371.      * @Route("/quicksearch", name="pimcore_admin_searchadmin_search_quicksearch", methods={"GET"})
  372.      *
  373.      * @param Request $request
  374.      * @param EventDispatcherInterface $eventDispatcher
  375.      *
  376.      * @return JsonResponse
  377.      */
  378.     public function quicksearchAction(Request $requestEventDispatcherInterface $eventDispatcher)
  379.     {
  380.         $query $this->filterQueryParam($request->get('query'''));
  381.         if (!preg_match('/[\+\-\*"]/'$query)) {
  382.             // check for a boolean operator (which was not filtered by filterQueryParam()),
  383.             // if present, do not add asterisk at the end of the query
  384.             $query $query '*';
  385.         }
  386.         $db \Pimcore\Db::get();
  387.         $searcherList = new Data\Listing();
  388.         $conditionParts = [];
  389.         $conditionParts[] = $this->getPermittedPaths();
  390.         $matchCondition '( MATCH (`data`,`properties`) AGAINST (' $db->quote($query) . ' IN BOOLEAN MODE) )';
  391.         $conditionParts[] = '(' $matchCondition " AND type != 'folder') ";
  392.         $queryCondition implode(' AND '$conditionParts);
  393.         $searcherList->setCondition($queryCondition);
  394.         $searcherList->setLimit(50);
  395.         $searcherList->setOrderKey($matchConditionfalse);
  396.         $searcherList->setOrder('DESC');
  397.         $beforeListLoadEvent = new GenericEvent($this, [
  398.             'list' => $searcherList,
  399.             'query' => $query,
  400.         ]);
  401.         $eventDispatcher->dispatch($beforeListLoadEventAdminEvents::QUICKSEARCH_LIST_BEFORE_LIST_LOAD);
  402.         $searcherList $beforeListLoadEvent->getArgument('list');
  403.         $hits $searcherList->load();
  404.         $elements = [];
  405.         foreach ($hits as $hit) {
  406.             $element Element\Service::getElementById($hit->getId()->getType(), $hit->getId()->getId());
  407.             if ($element->isAllowed('list')) {
  408.                 $data = [
  409.                     'id' => $element->getId(),
  410.                     'type' => $hit->getId()->getType(),
  411.                     'subtype' => $element->getType(),
  412.                     'className' => ($element instanceof DataObject\Concrete) ? $element->getClassName() : '',
  413.                     'fullpathList' => htmlspecialchars($this->shortenPath($element->getRealFullPath())),
  414.                 ];
  415.                 $this->addAdminStyle($elementElementAdminStyleEvent::CONTEXT_SEARCH$data);
  416.                 $elements[] = $data;
  417.             }
  418.         }
  419.         $afterListLoadEvent = new GenericEvent($this, [
  420.             'list' => $elements,
  421.             'context' => $query,
  422.         ]);
  423.         $eventDispatcher->dispatch($afterListLoadEventAdminEvents::QUICKSEARCH_LIST_AFTER_LIST_LOAD);
  424.         $elements $afterListLoadEvent->getArgument('list');
  425.         $result = ['data' => $elements'success' => true];
  426.         return $this->adminJson($result);
  427.     }
  428.     /**
  429.      * @Route("/quicksearch-get-by-id", name="pimcore_admin_searchadmin_search_quicksearch_by_id", methods={"GET"})
  430.      *
  431.      * @param Request $request
  432.      * @param Config $config
  433.      *
  434.      * @return JsonResponse
  435.      */
  436.     public function quicksearchByIdAction(Request $requestConfig $config)
  437.     {
  438.         $type $request->get('type');
  439.         $id $request->get('id');
  440.         $db \Pimcore\Db::get();
  441.         $searcherList = new Data\Listing();
  442.         $searcherList->addConditionParam('id = :id', ['id' => $id]);
  443.         $searcherList->addConditionParam('maintype = :type', ['type' => $type]);
  444.         $searcherList->setLimit(1);
  445.         $hits $searcherList->load();
  446.         //There will always be one result in hits but load returns array.
  447.         $data = [];
  448.         foreach ($hits as $hit) {
  449.             $element Element\Service::getElementById($hit->getId()->getType(), $hit->getId()->getId());
  450.             if ($element->isAllowed('list')) {
  451.                 $data = [
  452.                     'id' => $element->getId(),
  453.                     'type' => $hit->getId()->getType(),
  454.                     'subtype' => $element->getType(),
  455.                     'className' => ($element instanceof DataObject\Concrete) ? $element->getClassName() : '',
  456.                     'fullpath' => htmlspecialchars($element->getRealFullPath()),
  457.                     'fullpathList' => htmlspecialchars($this->shortenPath($element->getRealFullPath())),
  458.                     'iconCls' => 'pimcore_icon_asset_default',
  459.                 ];
  460.                 $this->addAdminStyle($elementElementAdminStyleEvent::CONTEXT_SEARCH$data);
  461.                 $validLanguages \Pimcore\Tool::getValidLanguages();
  462.                 $data['preview'] = $this->renderView(
  463.                     '@PimcoreAdmin/SearchAdmin/Search/Quicksearch/' $hit->getId()->getType() . '.html.twig', [
  464.                         'element' => $element,
  465.                         'iconCls' => $data['iconCls'],
  466.                         'config' => $config,
  467.                         'validLanguages' => $validLanguages,
  468.                     ]
  469.                 );
  470.             }
  471.         }
  472.         return $this->adminJson($data);
  473.     }
  474.     /**
  475.      * @param string $path
  476.      *
  477.      * @return string
  478.      */
  479.     protected function shortenPath($path)
  480.     {
  481.         $parts explode('/'trim($path'/'));
  482.         $count count($parts) - 1;
  483.         for ($i $count; ; $i--) {
  484.             $shortPath '/' implode('/'array_unique($parts));
  485.             if ($i === || strlen($shortPath) <= 50) {
  486.                 break;
  487.             }
  488.             array_splice($parts$i 11'…');
  489.         }
  490.         if (mb_strlen($shortPath) > 50) {
  491.             $shortPath mb_substr($shortPath049) . '…';
  492.         }
  493.         return $shortPath;
  494.     }
  495. }