PHRAS-3800_xss (#4219)

* add encode option to record::get_title ; render preview.record_title in twig

* html-escape facet values
This commit is contained in:
jygaulier
2023-03-15 11:05:34 +01:00
committed by GitHub
parent 32ff2739ab
commit e7027c7220
31 changed files with 241 additions and 158 deletions

View File

@@ -13,5 +13,5 @@ module.exports = {
setupDir: _root + 'tests/setup/node.js',
karmaConf: _root + 'config/karma.conf.js',
// change this version when you change JS file for lazy loading
assetFileVersion: 80
assetFileVersion: 81
};

View File

@@ -96,7 +96,7 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=80";
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=81";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {

View File

@@ -96,7 +96,7 @@ return /******/ (function(modules) { // webpackBootstrap
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=80";
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=81";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {

View File

@@ -91,7 +91,7 @@
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=80";
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".js?v=81";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {

View File

@@ -91,7 +91,7 @@
/******/ if (__webpack_require__.nc) {
/******/ script.setAttribute("nonce", __webpack_require__.nc);
/******/ }
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=80";
/******/ script.src = __webpack_require__.p + "lazy-" + ({}[chunkId]||chunkId) + ".min.js?v=81";
/******/ var timeout = setTimeout(onScriptComplete, 120000);
/******/ script.onerror = script.onload = onScriptComplete;
/******/ function onScriptComplete() {

View File

@@ -4006,11 +4006,13 @@ var workzoneFacets = function workzoneFacets(services) {
if (textLimit > 0 && textWithoutColorCode.length > textLimit) {
textWithoutColorCode = textWithoutColorCode.substring(0, textLimit) + '…';
}
textWithoutColorCode = (0, _jquery2.default)('<div/>').text(textWithoutColorCode).html(); // escape html
return '<span class="color-dot" style="background-color: ' + colorCode + '"></span>' + ' ' + textWithoutColorCode;
} else {
if (textLimit > 0 && string.length > textLimit) {
string = string.substring(0, textLimit) + '…';
}
string = (0, _jquery2.default)('<div/>').text(string).html(); // escape html
return string;
}
}

View File

@@ -4006,11 +4006,13 @@ var workzoneFacets = function workzoneFacets(services) {
if (textLimit > 0 && textWithoutColorCode.length > textLimit) {
textWithoutColorCode = textWithoutColorCode.substring(0, textLimit) + '…';
}
textWithoutColorCode = (0, _jquery2.default)('<div/>').text(textWithoutColorCode).html(); // escape html
return '<span class="color-dot" style="background-color: ' + colorCode + '"></span>' + ' ' + textWithoutColorCode;
} else {
if (textLimit > 0 && string.length > textLimit) {
string = string.substring(0, textLimit) + '…';
}
string = (0, _jquery2.default)('<div/>').text(string).html(); // escape html
return string;
}
}

View File

@@ -195,11 +195,12 @@ const workzoneFacets = services => {
return source;
}
function _formatColorText(string, textLimit = 0) {
function _formatColorText(string) {
var textLimit = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
//get color code from text if exist
var regexp = /^(.*)\[#([0-9a-fA-F]{6})].*$/;
var match = string.match(regexp);
if (match && match[2] != null) {
var colorCode = '#' + match[2];
@@ -208,11 +209,13 @@ const workzoneFacets = services => {
if (textLimit > 0 && textWithoutColorCode.length > textLimit) {
textWithoutColorCode = textWithoutColorCode.substring(0, textLimit) + '…';
}
textWithoutColorCode = $('<div/>').text(textWithoutColorCode).html(); // escape html
return '<span class="color-dot" style="background-color: ' + colorCode + '"></span>' + ' ' + textWithoutColorCode;
} else {
if (textLimit > 0 && string.length > textLimit) {
string = string.substring(0, textLimit) + '…';
}
string = $('<div/>').text(string).html(); // escape html
return string;
}
}

View File

@@ -97,6 +97,7 @@ use GuzzleHttp\Client as Guzzle;
use League\Fractal\Resource\Item;
use media_subdef;
use Neutron\TemporaryFilesystem\TemporaryFilesystemInterface;
use record_adapter;
use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
@@ -1056,7 +1057,7 @@ class V1Controller extends Controller
$ret = ['entity' => null];
if ($output instanceof \record_adapter) {
if ($output instanceof record_adapter) {
$ret['entity'] = '0';
$ret['url'] = '/records/' . $output->getDataboxId() . '/' . $output->getRecordId() . '/';
$this->dispatch(PhraseaEvents::RECORD_UPLOAD, new RecordEdit($output));
@@ -1133,7 +1134,7 @@ class V1Controller extends Controller
return Result::create($request, $ret)->createResponse();
}
private function listEmbeddableMedia(Request $request, \record_adapter $record, \media_subdef $media)
private function listEmbeddableMedia(Request $request, record_adapter $record, \media_subdef $media)
{
if (!$media->is_physically_present()) {
return null;
@@ -1653,10 +1654,10 @@ class V1Controller extends Controller
* Retrieve detailed information about one record
*
* @param Request $request
* @param \record_adapter $record
* @param record_adapter $record
* @return array
*/
private function listRecord(Request $request, \record_adapter $record)
private function listRecord(Request $request, record_adapter $record)
{
$technicalInformation = [];
foreach ($record->get_technical_infos()->getValues() as $name => $value) {
@@ -1667,7 +1668,7 @@ class V1Controller extends Controller
'databox_id' => $record->getDataboxId(),
'record_id' => $record->getRecordId(),
'mime_type' => $record->getMimeType(),
'title' => $record->get_title(),
'title' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'original_name' => $record->get_original_name(),
'updated_on' => $record->getUpdated()->format(DATE_ATOM),
'created_on' => $record->getCreated()->format(DATE_ATOM),
@@ -1696,11 +1697,11 @@ class V1Controller extends Controller
* Retrieve detailed information about one story
*
* @param Request $request
* @param \record_adapter $story
* @param record_adapter $story
* @return array
* @throws \Exception
*/
private function listStory(Request $request, \record_adapter $story)
private function listStory(Request $request, record_adapter $story)
{
if (!$story->isStory()) {
return Result::createError($request, 404, 'Story not found')->createResponse();
@@ -1796,10 +1797,10 @@ class V1Controller extends Controller
/**
* List all fields of given record
*
* @param \record_adapter $record
* @param record_adapter $record
* @return array
*/
private function listRecordMetadata(\record_adapter $record)
private function listRecordMetadata(record_adapter $record)
{
$includeBusiness = $this->getAclForUser()->can_see_business_fields($record->getDatabox());
@@ -1862,10 +1863,10 @@ class V1Controller extends Controller
/**
* Retrieve detailed information about one status
*
* @param \record_adapter $record
* @param record_adapter $record
* @return array
*/
private function listRecordStatus(\record_adapter $record)
private function listRecordStatus(record_adapter $record)
{
$ret = [];
foreach ($record->getStatusStructure() as $bit => $status) {
@@ -1896,7 +1897,7 @@ class V1Controller extends Controller
}, (array) $record->get_container_baskets($this->app['orm.em'], $this->getAuthenticatedUser()));
$stories = array_map(function (\record_adapter $story) use ($request) {
$stories = array_map(function (record_adapter $story) use ($request) {
return $this->listStory($request, $story);
}, array_values($record->get_grouping_parents()->get_elements()));
@@ -2551,7 +2552,7 @@ class V1Controller extends Controller
$stories[] = $this->createStory($data);
}
$result = Result::create($request, array('stories' => array_map(function(\record_adapter $story) {
$result = Result::create($request, array('stories' => array_map(function(record_adapter $story) {
return sprintf('/stories/%s/%s/', $story->getDataboxId(), $story->getRecordId());
}, $stories)));
@@ -2560,7 +2561,7 @@ class V1Controller extends Controller
/**
* @param object $data
* @return \record_adapter
* @return record_adapter
* @throws \Exception
*/
protected function createStory($data)
@@ -2571,7 +2572,7 @@ class V1Controller extends Controller
$this->app->abort(403, sprintf('You can not create a story on this collection %s', $collection->get_base_id()));
}
$story = \record_adapter::createStory($this->app, $collection);
$story = record_adapter::createStory($this->app, $collection);
if (isset($data->{'title'})) {
$story->set_original_name((string) $data->{'title'});
@@ -2633,7 +2634,7 @@ class V1Controller extends Controller
private function addOrDelStoryRecordsFromRequest(Request $request, $databox_id, $story_id, $action)
{
$data = $this->decodeJsonBody($request, 'story_records.json');
$story = new \record_adapter($this->app, $databox_id, $story_id);
$story = new record_adapter($this->app, $databox_id, $story_id);
$previousDescriptions = $story->getRecordDescriptionAsArray();
$records = $this->addOrDelStoryRecordsFromData($story, $data->story_records, $action);
@@ -2644,7 +2645,7 @@ class V1Controller extends Controller
return $result->createResponse();
}
private function addOrDelStoryRecordsFromData(\record_adapter $story, array $recordsData, $action)
private function addOrDelStoryRecordsFromData(record_adapter $story, array $recordsData, $action)
{
$records = array();
$cover_set = false;
@@ -2670,7 +2671,7 @@ class V1Controller extends Controller
return $records;
}
private function addOrDelStoryRecord(\record_adapter $story, $data, $action)
private function addOrDelStoryRecord(record_adapter $story, $data, $action)
{
$databox_id = $data->{'databox_id'};
$record_id = $data->{'record_id'};
@@ -2685,7 +2686,7 @@ class V1Controller extends Controller
}
try {
$record = new \record_adapter($this->app, $databox_id, $record_id);
$record = new record_adapter($this->app, $databox_id, $record_id);
} catch (\Exception_Record_AdapterNotFound $e) {
$record = null;
$this->app->abort(404, sprintf('Record identified by databox_is %s and record_id %s could not be found', $databox_id, $record_id));
@@ -2715,7 +2716,7 @@ class V1Controller extends Controller
{
$data = $this->decodeJsonBody($request, 'story_cover.json');
$story = new \record_adapter($this->app, $databox_id, $story_id);
$story = new record_adapter($this->app, $databox_id, $story_id);
$coverSource = [];
@@ -2733,7 +2734,7 @@ class V1Controller extends Controller
return Result::create($request, array($record_key))->createResponse();
}
protected function setStoryCover(\record_adapter $story, $fromChildRecordId, $can_fail=false, $coverSources = [])
protected function setStoryCover(record_adapter $story, $fromChildRecordId, $can_fail=false, $coverSources = [])
{
$coverSources = array_merge(['thumbnail_cover_source' => 'thumbnail', 'preview_cover_source' => 'preview'], $coverSources);
@@ -3339,10 +3340,10 @@ class V1Controller extends Controller
/**
* @param Request $request
* @param \record_adapter $record
* @param record_adapter $record
* @return array
*/
private function listRecordEmbeddableMedias(Request $request, \record_adapter $record)
private function listRecordEmbeddableMedias(Request $request, record_adapter $record)
{
$subdefs = [];
@@ -3356,10 +3357,10 @@ class V1Controller extends Controller
}
/**
* @param \record_adapter $record
* @param record_adapter $record
* @return array
*/
private function listRecordCaption(\record_adapter $record)
private function listRecordCaption(record_adapter $record)
{
$includeBusiness = $this->getAclForUser()->can_see_business_fields($record->getDatabox());
@@ -3395,7 +3396,7 @@ class V1Controller extends Controller
}
/**
* @param RecordCollection|\record_adapter[] $references
* @param RecordCollection|record_adapter[] $references
* @return RecordView[]
*/
private function buildRecordViews($references)

View File

@@ -152,7 +152,7 @@ class V3ResultHelpers
'databox_id' => $record->getDataboxId(),
'record_id' => $record->getRecordId(),
'mime_type' => $record->getMimeType(),
'title' => $record->get_title(),
'title' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'original_name' => $record->get_original_name(),
'updated_on' => $record->getUpdated()->format(DATE_ATOM),
'created_on' => $record->getCreated()->format(DATE_ATOM),

View File

@@ -22,6 +22,7 @@ use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Alchemy\Phrasea\Model\Repositories\BasketElementRepository;
use Alchemy\Phrasea\Model\Repositories\BasketRepository;
use Alchemy\Phrasea\Model\Repositories\TokenRepository;
use record_adapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -107,7 +108,7 @@ class LightboxController extends Controller
if ($this->app['browser']->isMobile()) {
return $this->renderResponse('lightbox/basket_element.html.twig', [
'basket_element' => $basketElement,
'module_name' => $basketElement->getRecord($this->app)->get_title(),
'module_name' => $basketElement->getRecord($this->app)->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'nextId' => $nextId,
'prevId' => $prevId
]);
@@ -116,7 +117,7 @@ class LightboxController extends Controller
$ret = [];
$ret['number'] = $basketElement->getRecord($this->app)->getNumber();
$ret['title'] = $basketElement->getRecord($this->app)->get_title();
$ret['title'] = $basketElement->getRecord($this->app)->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$ret['preview'] = $this->render(
'common/preview.html.twig',
@@ -157,13 +158,13 @@ class LightboxController extends Controller
if ($browser->isMobile()) {
return $this->renderResponse('lightbox/feed_element.html.twig', [
'feed_element' => $item,
'module_name' => $record->get_title()
'module_name' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE])
]);
}
$ret = [];
$ret['number'] = $record->getNumber();
$ret['title'] = $record->get_title();
$ret['title'] = $record->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$ret['preview'] = $this->render('common/preview.html.twig', [
'record' => $record,
'not_wrapped' => true,

View File

@@ -11,7 +11,6 @@
namespace Alchemy\Phrasea\Controller;
use Alchemy\Embed\Media\Media;
use Alchemy\Embed\Media\MediaInformation;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Application\Helper\ApplicationBoxAware;
use Alchemy\Phrasea\Authentication\ACLProvider;
@@ -20,6 +19,7 @@ use Alchemy\Phrasea\Core\Event\ExportEvent;
use Alchemy\Phrasea\Core\PhraseaEvents;
use Alchemy\Phrasea\Model\Repositories\BasketElementRepository;
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
use record_adapter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -90,7 +90,7 @@ class PermalinkController extends AbstractDelivery
'sbas_id' => $sbas_id,
'record_id' => $record_id,
'subdef' => $subdefName,
'label' => str_replace('/', '_', $record->get_title()),
'label' => str_replace('/', '_', $record->get_title(['encode'=> record_adapter::ENCODE_FOR_URI])),
'token' => $token,
]
);
@@ -171,14 +171,14 @@ class PermalinkController extends AbstractDelivery
/**
* @param Request $request
* @param \record_adapter $record
* @param record_adapter $record
* @param string $subdef
* @param bool $watermark
* @param bool $stamp
* @param string $token
* @return Response
*/
private function deliverContentWithCaptionLink(Request $request, \record_adapter $record, $subdef, $watermark, $stamp, $token)
private function deliverContentWithCaptionLink(Request $request, record_adapter $record, $subdef, $watermark, $stamp, $token)
{
$response = $this->deliverContent($request, $record, $subdef, $watermark, $stamp);
@@ -201,7 +201,7 @@ class PermalinkController extends AbstractDelivery
* @param string $token
* @param int $record_id
* @param string $subdef
* @return \record_adapter
* @return record_adapter
*/
private function retrieveRecord(\databox $databox, $token, $record_id, $subdef)
{

View File

@@ -14,6 +14,7 @@ use Alchemy\Phrasea\Controller\Controller;
use Alchemy\Phrasea\Controller\RecordsRequest;
use Alchemy\Phrasea\Helper\Record as RecordHelper;
use Alchemy\Phrasea\Out\Module\PDFRecords;
use record_adapter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
@@ -43,7 +44,7 @@ class PrinterController extends Controller
$r = RecordsRequest::fromRequest($this->app, $request, false);
if ($r->isSingleStory()) {
$pdfTitle = $r->singleStory()->get_title();
$pdfTitle = $r->singleStory()->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$storyId = $r->singleStory()->getId();
}

View File

@@ -23,7 +23,9 @@ use Alchemy\Phrasea\Model\Repositories\BasketElementRepository;
use Alchemy\Phrasea\Model\Repositories\StoryWZRepository;
use Alchemy\Phrasea\SearchEngine\SearchEngineOptions;
use Alchemy\Phrasea\Twig\PhraseanetExtension;
use record_adapter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -31,12 +33,14 @@ class RecordController extends Controller
{
use EntityManagerAware;
use SearchEngineAware;
/**
* Get record detailed view
*
* @param Request $request
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @return JsonResponse
* @throws \Exception
*/
public function getRecord(Request $request)
{
@@ -92,14 +96,7 @@ class RecordController extends Controller
}
$recordCaptions["technicalInfo"] = $record->getPositionFromTechnicalInfos();
// escape record title before rendering
$recordTitle = explode("</span>", $record->get_title());
if (count($recordTitle) >1) {
$recordTitle[1] = htmlspecialchars($recordTitle[1]);
$recordTitle = implode("</span>", $recordTitle);
} else {
$recordTitle = htmlspecialchars($record->get_title());
}
$recordTitle = $this->render('prod/preview/title.html.twig', ['record' => $record]);
$containerType = null;
@@ -207,7 +204,7 @@ class RecordController extends Controller
public function getRecordById($sbasId, $recordId)
{
$record = new \record_adapter($this->app, $sbasId, $recordId);
$record = new record_adapter($this->app, $sbasId, $recordId);
return $this->app->json([
"html_preview" => $this->render('common/preview.html.twig', [
'record' => $record
@@ -244,7 +241,7 @@ class RecordController extends Controller
$manager = $this->getEntityManager();
/** @var \record_adapter $record */
/** @var record_adapter $record */
foreach ($records as $record) {
try {
$basketElements = $basketElementsRepository->findElementsByRecord($record);
@@ -378,7 +375,7 @@ class RecordController extends Controller
$trashCollectionsBySbasId = [];
foreach ($records as $record) {
/** @var \record_adapter $record */
/** @var record_adapter $record */
$sbasId = $record->getDatabox()->get_sbas_id();
if (!array_key_exists($sbasId, $trashCollectionsBySbasId)) {
$trashCollectionsBySbasId[$sbasId] = $record->getDatabox()->getTrashCollection();
@@ -407,7 +404,7 @@ class RecordController extends Controller
*
* @param Request $request
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* @return JsonResponse
* @throws \Alchemy\Phrasea\Cache\Exception
*/
public function renewUrl(Request $request)

View File

@@ -28,6 +28,7 @@ use Alchemy\Phrasea\WorkerManager\Event\WorkerEvents;
use DataURI\Parser;
use MediaAlchemyst\Alchemyst;
use MediaVorus\MediaVorus;
use record_adapter;
use Symfony\Component\HttpFoundation\Request;
class ToolsController extends Controller
@@ -46,7 +47,7 @@ class ToolsController extends Controller
$recordAccessibleSubdefs = array();
$listsubdef= null;
if (count($records) == 1) {
/** @var \record_adapter $record */
/** @var record_adapter $record */
$record = $records->first();
/**Array list of subdefs**/
@@ -94,7 +95,7 @@ class ToolsController extends Controller
$availableSubdefName = [];
$countSubdefTodo = [];
/** @var \record_adapter $rec */
/** @var record_adapter $rec */
foreach ($records as $rec) {
$databoxSubdefs = $rec->getDatabox()->get_subdef_structure()->getSubdefGroup($rec->getType());
if ($databoxSubdefs !== null) {
@@ -218,7 +219,7 @@ class ToolsController extends Controller
throw new RuntimeException('Error while renaming file');
}
$record = new \record_adapter($this->app, $request->get('sbas_id'), $request->get('record_id'));
$record = new record_adapter($this->app, $request->get('sbas_id'), $request->get('record_id'));
$media = $this->app->getMediaFromUri($tempoFile);
@@ -278,7 +279,7 @@ class ToolsController extends Controller
throw new RuntimeException('Error while renaming file');
}
$record = new \record_adapter($this->app, $request->get('sbas_id'), $request->get('record_id'));
$record = new record_adapter($this->app, $request->get('sbas_id'), $request->get('record_id'));
$media = $this->app->getMediaFromUri($tempoFile);
@@ -306,9 +307,9 @@ class ToolsController extends Controller
$template = 'prod/actions/Tools/confirm.html.twig';
try {
$record = new \record_adapter($this->app, $request->request->get('sbas_id'), $request->request->get('record_id'));
$record = new record_adapter($this->app, $request->request->get('sbas_id'), $request->request->get('record_id'));
$var = [
'video_title' => $record->get_title(),
'video_title' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'image' => $request->request->get('image', ''),
];
$return = [
@@ -328,7 +329,7 @@ class ToolsController extends Controller
public function applyThumbnailExtractionAction(Request $request)
{
try {
$record = new \record_adapter($this->app, $request->request->get('sbas_id'), $request->request->get('record_id'));
$record = new record_adapter($this->app, $request->request->get('sbas_id'), $request->request->get('record_id'));
$subDef = $request->request->get('sub_def');
@@ -359,7 +360,7 @@ class ToolsController extends Controller
public function editRecordSharing(Request $request, $base_id, $record_id)
{
$record = new \record_adapter($this->app, \phrasea::sbasFromBas($this->app, $base_id), $record_id);
$record = new record_adapter($this->app, \phrasea::sbasFromBas($this->app, $base_id), $record_id);
$subdefName = (string)$request->request->get('name');
$state = $request->request->get('state') == 'true' ? true : false;
@@ -422,12 +423,12 @@ class ToolsController extends Controller
}
/**
* @param \record_adapter $record
* @param record_adapter $record
* @param string $subDefName
* @param string $subDefDataUri
* @throws \DataURI\Exception\InvalidDataException
*/
private function substituteMedia(\record_adapter $record, $subDefName, $subDefDataUri)
private function substituteMedia(record_adapter $record, $subDefName, $subDefDataUri)
{
$dataUri = Parser::parse($subDefDataUri);
@@ -456,7 +457,7 @@ class ToolsController extends Controller
*/
public function saveMetasAction(Request $request)
{
$record = new \record_adapter($this->app,
$record = new record_adapter($this->app,
(int)$request->request->get("databox_id"),
(int)$request->request->get("record_id"));
@@ -482,7 +483,7 @@ class ToolsController extends Controller
public function autoSubtitleAction(Request $request)
{
$record = new \record_adapter($this->app,
$record = new record_adapter($this->app,
(int)$request->request->get("databox_id"),
(int)$request->request->get("record_id")
);
@@ -522,7 +523,7 @@ class ToolsController extends Controller
$videoTextTrackFields = [];
if (count($records) == 1) {
/** @var \record_adapter $record */
/** @var record_adapter $record */
$record = $records->first();
$databox = $record->getDatabox();
@@ -568,7 +569,7 @@ class ToolsController extends Controller
]);
}
private function isPhysicallyPresent(\record_adapter $record, $subdefName)
private function isPhysicallyPresent(record_adapter $record, $subdefName)
{
try {
return $record->get_subdef($subdefName)->is_physically_present();

View File

@@ -12,9 +12,9 @@
namespace Alchemy\Phrasea\ControllerProvider\Prod;
use Alchemy\Phrasea\Application as PhraseaApplication;
use Alchemy\Phrasea\Core\LazyLocator;
use Alchemy\Phrasea\Controller\Prod\RecordController;
use Alchemy\Phrasea\ControllerProvider\ControllerProviderTrait;
use Alchemy\Phrasea\Core\LazyLocator;
use Silex\Application;
use Silex\ControllerProviderInterface;
use Silex\ServiceProviderInterface;
@@ -45,21 +45,26 @@ class Record implements ControllerProviderInterface, ServiceProviderInterface
{
$controllers = $this->createAuthenticatedCollection($app);
/** @uses RecordController::getRecord() */
$controllers->match('/', 'controller.prod.records:getRecord')
->bind('record_details')
->method('GET|POST');
/** @uses RecordController::getRecordById() */
$controllers->get('/record/{sbasId}/{recordId}/', 'controller.prod.records:getRecordById')
->bind('record_single')
->assert('sbasId', '\d+')
->assert('recordId', '\d+');
/** @uses RecordController::doDeleteRecords() */
$controllers->post('/delete/', 'controller.prod.records:doDeleteRecords')
->bind('record_delete');
/** @uses RecordController::whatCanIDelete() */
$controllers->post('/delete/what/', 'controller.prod.records:whatCanIDelete')
->bind('record_what_can_i_delete');
/** @uses RecordController::renewUrl() */
$controllers->post('/renew-url/', 'controller.prod.records:renewUrl')
->bind('record_renew_url');

View File

@@ -14,13 +14,14 @@ namespace Alchemy\Phrasea\Feed\Formatter;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Feed\FeedInterface;
use Alchemy\Phrasea\Feed\Link\FeedLink;
use Alchemy\Phrasea\Feed\Link\LinkGeneratorCollection;
use Alchemy\Phrasea\Feed\RSS\Image as FeedRSSImage;
use Alchemy\Phrasea\Model\Entities\FeedEntry;
use Alchemy\Phrasea\Model\Entities\FeedItem;
use Alchemy\Phrasea\Feed\Link\LinkGeneratorCollection;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Utilities\NullableDateTime;
use DateTime;
use record_adapter;
use Symfony\Component\HttpFoundation\Response;
class CoolirisFormatter extends FeedFormatterAbstract implements FeedFormatterInterface
@@ -208,7 +209,7 @@ class CoolirisFormatter extends FeedFormatterAbstract implements FeedFormatterIn
if (null !== $title_field) {
$str_title = $title_field->get_serialized_values(' ');
} else {
$str_title = $content->getRecord($app)->get_title();
$str_title = $content->getRecord($app)->get_title(['encode'=> record_adapter::ENCODE_FOR_HTML]);
}
//attach tile node to item node

View File

@@ -15,6 +15,7 @@ use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Model\Entities\StoryWZ;
use Alchemy\Phrasea\Model\Entities\User;
use Doctrine\ORM\EntityRepository;
use record_adapter;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -79,7 +80,7 @@ class StoryWZRepository extends EntityRepository
if ($story) {
try {
$story->getRecord($app)->get_title();
$story->getRecord($app)->get_title(['encode'=> record_adapter::ENCODE_NONE]);
} catch (NotFoundHttpException $e) {
$this->getEntityManager()->remove($story);
throw new NotFoundHttpException('Story not found');
@@ -95,7 +96,7 @@ class StoryWZRepository extends EntityRepository
return $story;
}
public function findUserStory(Application $app, User $user, \record_adapter $Story)
public function findUserStory(Application $app, User $user, record_adapter $Story)
{
$story = $this->findOneBy([
'user' => $user->getId(),
@@ -118,10 +119,10 @@ class StoryWZRepository extends EntityRepository
/**
* @param Application $app
* @param \record_adapter $Story
* @param record_adapter $Story
* @return StoryWZ[]
*/
public function findByRecord(Application $app, \record_adapter $Story)
public function findByRecord(Application $app, record_adapter $Story)
{
$dql = 'SELECT s FROM Phraseanet:StoryWZ s
WHERE s.sbas_id = :sbas_id

View File

@@ -10,9 +10,11 @@
namespace Alchemy\Phrasea\Model\Serializer;
use record_adapter;
class ESRecordSerializer extends AbstractSerializer
{
public function serialize(\record_adapter $record)
public function serialize(record_adapter $record)
{
$caption = $business = $status = [];
@@ -61,7 +63,7 @@ class ESRecordSerializer extends AbstractSerializer
'collection_id' => $record->getCollectionId(),
'base_id' => $record->getBaseId(),
'mime_type' => $record->getMimeType(),
'title' => $record->get_title(),
'title' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'original_name' => $record->get_original_name(),
'updated_on' => $record->getUpdated()->format(DATE_ATOM),
'created_on' => $record->getCreated()->format(DATE_ATOM),

View File

@@ -4,7 +4,8 @@ namespace Alchemy\Phrasea\Out\Module;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Out\Tool\PhraseaPDF;
use \IntlDateFormatter as DateFormatter;
use IntlDateFormatter as DateFormatter;
use record_adapter;
class PDFCgu extends PDF
{
@@ -102,8 +103,8 @@ class PDFCgu extends PDF
$ndoc = 0;
foreach ($this->recordIds as $recordId) {
/* @var \record_adapter $rec */
$rec = new \record_adapter($this->app, $this->databoxId, $recordId);
/* @var record_adapter $rec */
$rec = new record_adapter($this->app, $this->databoxId, $recordId);
$subdef = $rec->get_subdef('thumbnail');
$fimg = $subdef->getRealPath();
@@ -131,7 +132,7 @@ class PDFCgu extends PDF
if ($this->pdf->GetY() > $this->pdf->getPageHeight() - (6 + $finalHeight + 20))
$this->pdf->AddPage();
$title = "record : " . $rec->get_title();
$title = "record : " . $rec->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$y = $this->pdf->GetY();

View File

@@ -12,11 +12,12 @@
namespace Alchemy\Phrasea\Out\Module;
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Media\MediaSubDefinitionUrlGenerator;
use Alchemy\Phrasea\Out\Tool\PhraseaPDF;
use Alchemy\Phrasea\Helper\Record\Printer;
use Alchemy\Phrasea\Media\MediaSubDefinitionUrlGenerator;
use Alchemy\Phrasea\Model\Entities\ValidationParticipant;
use \IntlDateFormatter as DateFormatter;
use Alchemy\Phrasea\Out\Tool\PhraseaPDF;
use IntlDateFormatter as DateFormatter;
use record_adapter;
class PDFRecords extends PDF
{
@@ -200,7 +201,7 @@ class PDFRecords extends PDF
$irow = $ipage = 0;
$icol = -1;
foreach ($this->records as $rec) {
/* @var \record_adapter $rec */
/* @var record_adapter $rec */
if (++$icol >= $NDiapoW) {
$icol = 0;
if (++$irow >= $NDiapoH) {
@@ -284,7 +285,7 @@ class PDFRecords extends PDF
);
}
$downloadLink = $rec->get_title();
$downloadLink = $rec->get_title(['encode'=> record_adapter::ENCODE_FOR_URI]);
if ($this->canDownload && !empty($this->downloadSubdef) && $rec->has_subdef($this->downloadSubdef)
@@ -295,7 +296,7 @@ class PDFRecords extends PDF
$sd = $rec->get_subdef($this->downloadSubdef);
if ($sd->is_physically_present()) {
$url = $this->getDownloadUrl($sd);
$downloadLink = sprintf('<a style="text-decoration: none;" href="%s">%s</a>', $url, $rec->get_title());
$downloadLink = sprintf('<a style="text-decoration: none;" href="%s">%s</a>', $url, $rec->get_title(['encode'=> record_adapter::ENCODE_FOR_HTML]));
}
}
@@ -337,7 +338,7 @@ class PDFRecords extends PDF
$this->pdf->SetLeftMargin($lmargin + 55);
$ndoc = 0;
/* @var \record_adapter $rec */
/* @var record_adapter $rec */
foreach ($this->records as $rec) {
$subdef = null;
@@ -393,7 +394,7 @@ class PDFRecords extends PDF
if ($this->pdf->GetY() > $this->pdf->getPageHeight() - (6 + $finalHeight + 20))
$this->pdf->AddPage();
$title = "record : " . $rec->get_title();
$title = "record : " . $rec->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$y = $this->pdf->GetY();
@@ -495,9 +496,9 @@ class PDFRecords extends PDF
$lmargin = $oldMargins['left'];
$rmargin = $oldMargins['right'];
/* @var \record_adapter $rec */
/* @var record_adapter $rec */
foreach ($this->records as $rec) {
$title = "record : " . $rec->get_title();
$title = "record : " . $rec->get_title(['encode'=> record_adapter::ENCODE_NONE]);
$y = $this->pdf->GetY();
if($this->pdf->getPageHeight() - $y < 30){ // height of the footer is 15
@@ -640,7 +641,7 @@ class PDFRecords extends PDF
}
foreach ($this->records as $krec => $rec) {
/* @var \record_adapter $rec */
/* @var record_adapter $rec */
$this->pdf->AddPage();
@@ -763,7 +764,7 @@ class PDFRecords extends PDF
$subdef = null;
if ($rec->has_subdef($this->previewName)) {
/* @var \record_adapter $rec */
/* @var record_adapter $rec */
$subdef = $rec->get_subdef($this->previewName);
}
@@ -938,7 +939,7 @@ class PDFRecords extends PDF
return;
}
private function showRecordInfoBloc(\record_adapter $rec)
private function showRecordInfoBloc(record_adapter $rec)
{
$r = $g = $b = 0;
if (!empty($this->fieldTitleColor)) {
@@ -949,7 +950,7 @@ class PDFRecords extends PDF
$this->pdf->Write(5, $this->app->trans("print_feedback:: record title: ") . " ");
$this->pdf->SetTextColor(0);
$this->pdf->SetFont(PhraseaPDF::FONT, '', $this->descriptionFontSize);
$this->pdf->Write(5, $rec->get_title());
$this->pdf->Write(5, $rec->get_title(['encode'=> record_adapter::ENCODE_NONE]));
$this->pdf->Write(6, "\n");
if (!empty($this->fieldTitleColor)) {
@@ -1096,7 +1097,7 @@ class PDFRecords extends PDF
$infos = pathinfo($subdef->getRealPath());
if ($this->printer->getTitleAsDownloadName()) {
$filename = mb_strtolower(mb_substr($subdef->get_record()->get_title(['removeExtension' => true]), 0, self::$maxFilenameLength), 'UTF-8');
$filename = mb_strtolower(mb_substr($subdef->get_record()->get_title(['removeExtension' => true, 'encode'=> record_adapter::ENCODE_FOR_URI]), 0, self::$maxFilenameLength), 'UTF-8');
} else {
$originalName = $subdef->get_record()->get_original_name(true);
$originalName = empty($originalName) ? $subdef->get_record()->getId() : $originalName;

View File

@@ -11,6 +11,7 @@
namespace Alchemy\Phrasea\Search;
use League\Fractal\TransformerAbstract;
use record_adapter;
class RecordTransformer extends TransformerAbstract
{
@@ -55,7 +56,7 @@ class RecordTransformer extends TransformerAbstract
'databox_id' => $record->getDataboxId(),
'record_id' => $record->getRecordId(),
'mime_type' => $record->getMimeType(),
'title' => $record->get_title(),
'title' => $record->get_title(['encode'=> record_adapter::ENCODE_NONE]),
'original_name' => $record->get_original_name(),
'updated_on' => $record->getUpdated()->format(DATE_ATOM),
'created_on' => $record->getCreated()->format(DATE_ATOM),

View File

@@ -60,7 +60,7 @@ class PhraseanetExtension extends \Twig_Extension
{
return [
// change this version when you change JS file to force the navigation to reload js file
'assetFileVersion' => 80
'assetFileVersion' => 81
];
}

View File

@@ -645,7 +645,7 @@ class Bridge_Api_Youtube extends Bridge_Api_Abstract implements Bridge_Api_Inter
$filesource = new Zend_Gdata_App_MediaFileSource($record->get_hd_file()->getRealPath());
$filesource->setContentType($record->get_hd_file()->get_mime());
$filesource->setSlug($record->get_title());
$filesource->setSlug($record->get_title(['encode'=> record_adapter::ENCODE_FOR_URI]));
$video_entry->setMediaSource($filesource);
$video_entry->setVideoTitle($options['title']);

View File

@@ -44,7 +44,7 @@ class eventsmanager_notify_bridgeuploadfail extends eventsmanager_notifyAbstract
$ret = [
'text' => $this->app->trans("L'upload concernant le record %title% sur le compte %bridge_name% a echoue pour les raisons suivantes : %reason%", [
'%title%' => $record->get_title(),
'%title%' => $record->get_title(['encode'=> record_adapter::ENCODE_FOR_HTML]),
'%bridge_name%' => $account->get_api()->get_connector()->get_name(),
'%reason%' => $reason
])

View File

@@ -10,15 +10,13 @@
*/
use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Exception\RuntimeException;
use Alchemy\Phrasea\Model\Manipulator\TokenManipulator;
use Alchemy\Phrasea\Utilities\NullableDateTime;
use Assert\Assertion;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Guzzle\Http\Url;
use \RandomLib\Generator;
use RandomLib\Generator;
class media_Permalink_Adapter implements cache_cacheableInterface
{
@@ -396,7 +394,7 @@ class media_Permalink_Adapter implements cache_cacheableInterface
// build a multi-rows insert
$inserts = '';
// constant part values
$insk = ", 1, NOW(), NOW(), " . $connection->quote(self::cleanLabel($unicode, $record->get_title(['removeExtension' => true])));
$insk = ", 1, NOW(), NOW(), " . $connection->quote(self::cleanLabel($unicode, $record->get_title(['removeExtension' => true, 'encode'=> record_adapter::ENCODE_FOR_URI])));
// multiple rows
foreach($subdef_ids as $subdef_id) {
// fake subdefs (icons substitution) for thumb/prev are hardcoded.
@@ -487,7 +485,7 @@ class media_Permalink_Adapter implements cache_cacheableInterface
'token' => $generator->generateString(64, TokenManipulator::LETTERS_AND_NUMBERS),
'label' => self::cleanLabel(
$unicode,
$records[$media_subdef->get_record_id()]->get_title(['removeExtension' => true])
$records[$media_subdef->get_record_id()]->get_title(['removeExtension' => true, 'encode'=> record_adapter::ENCODE_FOR_URI])
),
];
}

View File

@@ -519,7 +519,7 @@ class module_report_nav extends module_report
, 'record_id' => $record->getRecordId()
, 'date' => $this->app['date-formatter']->getPrettyString($document->get_creation_date())
, 'type' => $document->get_mime()
, 'titre' => $record->get_title()
, 'titre' => $record->get_title(['encode'=> record_adapter::ENCODE_FOR_HTML])
, 'taille' => $document->get_size()
];

View File

@@ -32,10 +32,10 @@ use Alchemy\Phrasea\Media\TechnicalData;
use Alchemy\Phrasea\Media\TechnicalDataSet;
use Alchemy\Phrasea\Metadata\Tag\TfBasename;
use Alchemy\Phrasea\Metadata\Tag\TfFilename;
use Alchemy\Phrasea\Model\Repositories\FeedItemRepository;
use Alchemy\Phrasea\Model\Entities\OrderElement;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Model\RecordInterface;
use Alchemy\Phrasea\Model\Repositories\FeedItemRepository;
use Alchemy\Phrasea\Model\Serializer\CaptionSerializer;
use Alchemy\Phrasea\Record\RecordReference;
use Alchemy\Phrasea\Twig\PhraseanetExtension;
@@ -63,6 +63,10 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
const CACHE_SUBDEFS = 'subdefs';
const CACHE_GROUPING = 'grouping';
const ENCODE_NONE = 'encode_none';
const ENCODE_FOR_HTML = 'encode_for_html';
const ENCODE_FOR_URI = 'encode_for_uri';
/**
* @param Application $app
* @return FilesystemService
@@ -974,7 +978,7 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
public function getTitle($locale = null, Array $options = [])
{
$removeExtension = !!igorw\get_in($options, ['removeExtension'], false);
$encode = igorw\get_in($options, ['encode'], self::ENCODE_NONE);
$cache = !$removeExtension;
if ($cache) {
@@ -1002,7 +1006,16 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
$titles = [];
foreach ($retrieved_fields as $value) {
foreach ($value['values'] as $v) {
$titles[] = $v['value'];
$v = $v['value'];
switch ($encode) {
case self::ENCODE_FOR_HTML:
$v = htmlentities($v);
break;
case self::ENCODE_FOR_URI:
$v = urlencode($v);
break;
}
$titles[] = $v;
}
}
$title = trim(implode(' - ', $titles));
@@ -1010,6 +1023,14 @@ class record_adapter implements RecordInterface, cache_cacheableInterface
if (trim($title) === '') {
$title = trim($this->get_original_name($removeExtension));
switch ($encode) {
case self::ENCODE_FOR_HTML:
$title = htmlentities($title);
break;
case self::ENCODE_FOR_URI:
$title = urlencode($title);
break;
}
}
$title = $title != "" ? $title : $this->app->trans('reponses::document sans titre');

View File

@@ -119,7 +119,7 @@ class record_preview extends record_adapter
$this->container = new record_adapter($app, $sbas_id, $record_id);
$this->original_item = $this->container;
$this->name = $this->container->get_title();
$this->name = $this->container->get_title(['encode'=> record_adapter::ENCODE_NONE]);
if ($pos == 0) {
$number = 0;
} else {
@@ -253,51 +253,66 @@ class record_preview extends record_adapter
return $this->original_item;
}
/**
*
* @return String
*/
public function get_title(Array $options = [])
public function getEnv()
{
if ($this->title) {
return $this->title;
}
$this->title = '';
switch ($this->env) {
case "RESULT":
$this->title = '<span style="color:#27bbe2;">';
$this->title .= $this->app->trans('Resultat %number% / %total%', ['%number%' => '<span id="current_result_n">' . $this->formatNumber($this->getNumber() + 1) . '</span>', '%total%' => $this->formatNumber($this->total)]);
$this->title .= ' : </span> ' . parent::get_title($options);
break;
case "BASK":
$this->title = '<span style="color:#27bbe2;">';
$this->title .= $this->name . ' (' . $this->formatNumber($this->getNumber()) . ' / ' . $this->formatNumber($this->total) . ') : </span>' . parent::get_title($options);
break;
case "REG":
$this->title = '<span style="color:#27bbe2;">';
$this->title .= $this->name;
if ($this->getNumber() != 0) {
$this->title .= sprintf(
' (%s) : </span> %s',$this->formatNumber($this->getNumber()) . ' / ' . $this->formatNumber($this->total), parent::get_title($options)
);
} else {
$this->title .= '</span>';
}
break;
default:
$this->title .= parent::get_title($options);
break;
}
return $this->title;
return $this->env;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
// PHRAS-3800 : html is now done in twig, so getting parent::get_title is fine
// --> no more overload
// /**
// *
// * @return String
// */
// public function old_get_title(Array $options = [])
// {
// if ($this->title) {
// return $this->title;
// }
//
// $this->title = '';
//
// switch ($this->env) {
//
// case "RESULT":
// $this->title = '<span style="color:#27bbe2;">';
// $this->title .= $this->app->trans('Resultat %number% / %total%', ['%number%' => '<span id="current_result_n">' . $this->formatNumber($this->getNumber() + 1) . '</span>', '%total%' => $this->formatNumber($this->total)]);
// $this->title .= ' : </span> ' . parent::get_title($options);
// break;
// case "BASK":
// $this->title = '<span style="color:#27bbe2;">';
// $this->title .= $this->name . ' (' . $this->formatNumber($this->getNumber()) . ' / ' . $this->formatNumber($this->total) . ') : </span>' . parent::get_title($options);
//
// break;
// case "REG":
// $this->title = '<span style="color:#27bbe2;">';
// $this->title .= $this->name;
//
// if ($this->getNumber() != 0) {
// $this->title .= sprintf(
// ' (%s) : </span> %s',$this->formatNumber($this->getNumber()) . ' / ' . $this->formatNumber($this->total), parent::get_title($options)
// );
// } else {
// $this->title .= '</span>';
// }
//
// break;
// default:
// $this->title .= parent::get_title($options);
// break;
// }
//
// return $this->title;
// }
/**
* @return mixed content
*/
@@ -788,7 +803,7 @@ class record_preview extends record_adapter
return $this->download_popularity;
}
private function formatNumber($number)
public function formatNumber($number)
{
return number_format($number, 0, null, ' ');
}

View File

@@ -138,7 +138,7 @@ class set_export extends set_abstract
$app,
$child_basrec->getDataboxId(),
$record_id,
$record->get_title(['removeExtension' => true]) . '_' . $n,
$record->get_title(['removeExtension' => true, 'encode'=> record_adapter::ENCODE_NONE]) . '_' . $n,
$remain_hd[$base_id]
);
$this->add_element($current_element);
@@ -467,7 +467,7 @@ class set_export extends set_abstract
//
if ($rename_title) {
// use the title (may be a concat of fields)
$export_name = strip_tags($download_element->get_title(['removeExtension' => true]));
$export_name = strip_tags($download_element->get_title(['removeExtension' => true, 'encode'=> record_adapter::ENCODE_FOR_URI]));
// if the "title" ends up with a "filename-like" field, remove extension
if (strtolower(substr($export_name, -strlen($extension)-1)) === '.'.strtolower($extension)) {
$export_name = substr($export_name, 0, strlen($export_name)-1-strlen($extension));

View File

@@ -0,0 +1,29 @@
{# @var \record_preview record #}
{% if record.env == "RESULT" %}
<span style="color:#27bbe2;">
{% trans with {
'%number%' : '<span id="current_result_n">' ~ record.formatNumber(record.getNumber() + 1) ~ '</span>',
'%total%' : record.formatNumber(record.total)
} %}
Resultat %number% / %total%
{% endtrans %}
</span>
{{ record.get_title() }}
{% elseif record.env == "BASK" %}
<span style="color:#27bbe2;">
{{ record.name }}
({{ record.formatNumber(record.getNumber()) }} / {{ record.formatNumber(record.total) }})
</span>
{{ record.get_title() }}
{% elseif record.env == "REG" %}
<span style="color:#27bbe2;">
{{ record.name }}
{% if record.getNumber() != 0 %}
({{ record.formatNumber(record.getNumber()) }} / {{ record.formatNumber(record.total) }})</span>
{{ record.get_title() }}
{% else %}
</span>
{% endif %}
{% else %}
{{ record.get_title() }}
{% endif %}