mirror of
https://github.com/alchemy-fr/Phraseanet.git
synced 2025-10-23 18:03:17 +00:00
Apply ACL restriction filters for a search query
This commit is contained in:
@@ -11,7 +11,6 @@
|
||||
|
||||
namespace Alchemy\Phrasea\SearchEngine\Elastic;
|
||||
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\ElasticsearchRecordHydrator;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\RecordIndexer;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Indexer\TermIndexer;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\Search\SearchQuery;
|
||||
@@ -26,6 +25,10 @@ use Elasticsearch\Client;
|
||||
|
||||
class ElasticSearchEngine implements SearchEngineInterface
|
||||
{
|
||||
const FLAG_ALLOW_BOTH = 'allow_both';
|
||||
const FLAG_SET_ONLY = 'set_only';
|
||||
const FLAG_UNSET_ONLY = 'unset_only';
|
||||
|
||||
private $app;
|
||||
/** @var Client */
|
||||
private $client;
|
||||
@@ -438,7 +441,7 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function createRecordQueryParams($ESquery, SearchEngineOptions $options, \record_adapter $record = null)
|
||||
private function createRecordQueryParams($ESQuery, SearchEngineOptions $options, \record_adapter $record = null)
|
||||
{
|
||||
$params = [
|
||||
'index' => $this->indexName,
|
||||
@@ -448,86 +451,54 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
]
|
||||
];
|
||||
|
||||
$filters = $this->createFilters($options);
|
||||
$query_filters = $this->createQueryFilters($options);
|
||||
$acl_filters = $this->createACLFilters();
|
||||
|
||||
if ($record) {
|
||||
$filters[] = [
|
||||
'term' => [
|
||||
'_id' => sprintf('%s-%s', $record->get_sbas_id(), $record->get_record_id()),
|
||||
]
|
||||
];
|
||||
$ESQuery = ['filtered' => ['query' => $ESQuery]];
|
||||
|
||||
$fields = [];
|
||||
|
||||
foreach ($record->get_databox()->get_meta_structure() as $dbField) {
|
||||
$fields['caption.'.$dbField->get_name()] = new \stdClass();
|
||||
if (count($query_filters) > 0) {
|
||||
$ESQuery['filtered']['filter']['bool']['must'][] = $query_filters;
|
||||
}
|
||||
|
||||
$params['body']['highlight'] = [
|
||||
"pre_tags" => ["[[em]]"],
|
||||
"post_tags" => ["[[/em]]"],
|
||||
"fields" => $fields,
|
||||
];
|
||||
if (count($acl_filters) > 0) {
|
||||
$ESQuery['filtered']['filter']['bool']['must'][] = $acl_filters;
|
||||
}
|
||||
|
||||
if (count($filters) > 0) {
|
||||
$ESquery = [
|
||||
'filtered' => [
|
||||
'query' => $ESquery,
|
||||
'filter' => [
|
||||
'and' => $filters
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$params['body']['query'] = $ESquery;
|
||||
$params['body']['query'] = $ESQuery;
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
private function createFilters(SearchEngineOptions $options)
|
||||
private function createACLFilters()
|
||||
{
|
||||
// No ACLs if no user
|
||||
if (false === $this->app['authentication']->isAuthenticated()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$acl = $this->app['acl']->get($this->app['authentication']->getUser());
|
||||
|
||||
$grantedCollections = array_keys($acl->get_granted_base(['actif']));
|
||||
|
||||
if (count($grantedCollections) === 0) {
|
||||
return ['bool' => ['must_not' => ['match_all' => new \stdClass()]]];
|
||||
}
|
||||
|
||||
$flagNamesMap = $this->getFlagsKey($this->app['phraseanet.appbox']);
|
||||
// Get flags rules
|
||||
$flagRules = $this->getFlagsRules($acl, $grantedCollections);
|
||||
// Get intersection between collection ACLs and collection chosen by end user
|
||||
$aclRules = $this->getACLsByCollection($flagRules, $flagNamesMap);
|
||||
|
||||
return $this->buildACLsFilters($aclRules);
|
||||
}
|
||||
|
||||
private function createQueryFilters(SearchEngineOptions $options)
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
$status_opts = $options->getStatus();
|
||||
foreach ($options->getDataboxes() as $databox) {
|
||||
foreach ($databox->get_statusbits() as $bit => $status) {
|
||||
if (!array_key_exists($bit, $status_opts)) {
|
||||
continue;
|
||||
}
|
||||
if (!array_key_exists($databox->get_sbas_id(), $status_opts[$bit])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = RecordIndexer::normalizeFlagKey($status['labelon']);
|
||||
$filters[] = [
|
||||
'term' => [
|
||||
sprintf('flags.%s', $key) => (bool) $status_opts[$bit][$databox->get_sbas_id()],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$base_ids = [];
|
||||
foreach ($options->getCollections() as $collection) {
|
||||
$base_ids[] = $collection->get_base_id();
|
||||
}
|
||||
|
||||
if (count($base_ids) > 0) {
|
||||
$filters[] = [
|
||||
'terms' => [
|
||||
'base_id' => array_values(array_filter($base_ids))
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$filters[] = [
|
||||
'term' => [
|
||||
'record_type' => $options->getSearchType() === SearchEngineOptions::RECORD_RECORD ?
|
||||
SearchEngineInterface::GEM_TYPE_RECORD : SearchEngineInterface::GEM_TYPE_STORY,
|
||||
]
|
||||
];
|
||||
$filters[]['term']['record_type'] = $options->getSearchType() === SearchEngineOptions::RECORD_RECORD ?
|
||||
SearchEngineInterface::GEM_TYPE_RECORD : SearchEngineInterface::GEM_TYPE_STORY;
|
||||
|
||||
if ($options->getDateFields() && ($options->getMaxDate() || $options->getMinDate())) {
|
||||
$range = [];
|
||||
@@ -539,20 +510,40 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
}
|
||||
|
||||
foreach ($options->getDateFields() as $dateField) {
|
||||
$filters[] = [
|
||||
'range' => [
|
||||
'caption.'.$dateField->get_name() => $range
|
||||
]
|
||||
];
|
||||
$filters[]['range']['caption.'.$dateField->get_name()] = $range;
|
||||
}
|
||||
}
|
||||
|
||||
if ($options->getRecordType()) {
|
||||
$filters[] = [
|
||||
'term' => [
|
||||
'phrasea_type' => $options->getRecordType(),
|
||||
]
|
||||
];
|
||||
$filters[]['term']['phrasea_type'] = $options->getRecordType();
|
||||
}
|
||||
|
||||
if (count($options->getCollections()) > 0) {
|
||||
$filters[]['terms']['base_id'] = array_map(function($collection) {
|
||||
return $collection->get_base_id();
|
||||
}, $options->getCollections());
|
||||
}
|
||||
|
||||
if (count($options->getStatus()) > 0) {
|
||||
$status_filters = [];
|
||||
$flagNamesMap = $this->getFlagsKey($this->app['phraseanet.appbox']);
|
||||
|
||||
foreach ($options->getStatus() as $databoxId => $status) {
|
||||
$status_filter = $databox_status =[];
|
||||
$status_filter[] = ['term' => ['databox_id' => $databoxId]];
|
||||
foreach ($status as $n => $v) {
|
||||
if (!isset($flagNamesMap[$databoxId][$n])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $flagNamesMap[$databoxId][$n];
|
||||
$databox_status[] = ['term' => [sprintf('flags.%s', $label) => (bool) $v]];
|
||||
};
|
||||
$status_filter[] = $databox_status;
|
||||
|
||||
$status_filters[] = ['bool' => ['must' => $status_filter]];
|
||||
}
|
||||
$filters[]['bool']['should'] = $status_filters;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
@@ -610,4 +601,120 @@ class ElasticSearchEngine implements SearchEngineInterface
|
||||
|
||||
return $fieldsExpended;
|
||||
}
|
||||
|
||||
private function getFlagsKey(\appbox $appbox)
|
||||
{
|
||||
$flags = [];
|
||||
foreach ($appbox->get_databoxes() as $databox) {
|
||||
$databoxId = $databox->get_sbas_id();
|
||||
$status = $databox->get_statusbits();
|
||||
foreach($status as $bit => $stat) {
|
||||
$flags[$databoxId][$bit] = RecordHelper::normalizeFlagKey($stat['labelon']);
|
||||
}
|
||||
}
|
||||
|
||||
return $flags;
|
||||
}
|
||||
|
||||
private function getFlagsRules(\ACL $acl, array $collections)
|
||||
{
|
||||
$rules = [];
|
||||
foreach ($collections as $collectionId) {
|
||||
$databoxId = \phrasea::sbasFromBas($this->app, $collectionId);
|
||||
foreach (range(0, 31) as $bit) {
|
||||
$rules[$databoxId][$collectionId][$bit] = $this->computeAccess(
|
||||
$acl->get_mask_xor($collectionId),
|
||||
$acl->get_mask_and($collectionId),
|
||||
$bit
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truth table for status rights
|
||||
*
|
||||
* +-----------+
|
||||
* | and | xor |
|
||||
* +-----------+
|
||||
* | 0 | 0 | -> BOTH STATES ARE CHECKED
|
||||
* +-----------+
|
||||
* | 1 | 0 | -> UNSET STATE IS CHECKED
|
||||
* +-----------+
|
||||
* | 0 | 1 | -> UNSET STATE IS CHECKED (not possible)
|
||||
* +-----------+
|
||||
* | 1 | 1 | -> SET STATE IS CHECKED
|
||||
* +-----------+
|
||||
*
|
||||
*/
|
||||
private function computeAccess($and, $xor, $bit)
|
||||
{
|
||||
$xorBit = \databox_status::bitIsSet($xor, $bit);
|
||||
$andBit = \databox_status::bitIsSet($and, $bit);
|
||||
|
||||
if (!$xorBit && !$andBit) {
|
||||
return self::FLAG_ALLOW_BOTH;
|
||||
}
|
||||
|
||||
if ($xorBit && $andBit) {
|
||||
return self::FLAG_SET_ONLY;
|
||||
}
|
||||
|
||||
// otherwise there is a restriction for this status when it is not raised
|
||||
return self::FLAG_UNSET_ONLY;
|
||||
}
|
||||
|
||||
private function getACLsByCollection(array $flagACLs, array $flagNamesMap)
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
foreach ($flagACLs as $databoxId => $bases) {
|
||||
foreach ($bases as $baseId => $bit) {
|
||||
$rules[$baseId] = [];
|
||||
foreach ($bit as $n => $rule) {
|
||||
if (!isset($flagNamesMap[$databoxId][$n])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$label = $flagNamesMap[$databoxId][$n];
|
||||
$rules[$baseId][$label] = $rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
private function buildACLsFilters(array $aclRules)
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
foreach ($aclRules as $baseId => $flagsRules) {
|
||||
$ruleFilter = $baseFilter = [];
|
||||
|
||||
// filter on base
|
||||
$baseFilter['term']['base_id'] = $baseId;
|
||||
$ruleFilter['bool']['must'][] = $baseFilter;
|
||||
|
||||
// filter by flags
|
||||
foreach ($flagsRules as $flagName => $flagRule) {
|
||||
// only add filter if one of the status state is not allowed / allowed
|
||||
if ($flagRule === self::FLAG_ALLOW_BOTH) {
|
||||
continue;
|
||||
}
|
||||
$flagFilter = [];
|
||||
|
||||
$flagField = sprintf('flags.%s', $flagName);
|
||||
$flagFilter['term'][$flagField] = $flagRule === self::FLAG_SET_ONLY ? true : false;
|
||||
|
||||
$ruleFilter['bool']['must'][] = $flagFilter;
|
||||
}
|
||||
|
||||
$filters[] = $ruleFilter;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ class ElasticsearchRecordHydrator
|
||||
$updatedOn = igorw\get_in($data, ['updated_on']);
|
||||
$record->setUpdated($updatedOn ? new \DateTime($updatedOn) : $updatedOn);
|
||||
$record->setUuid(igorw\get_in($data, ['uuid'], ''));
|
||||
$record->setStatus(igorw\get_in($data, ['bin_status'], str_repeat('0', 32)));
|
||||
$record->setStatus(igorw\get_in($data, ['flags_bitmask'], 0));
|
||||
$record->setTitles(new ArrayCollection((array) igorw\get_in($data, ['title'], [])));
|
||||
$record->setCaption(new ArrayCollection((array) igorw\get_in($data, ['caption'], [])));
|
||||
$record->setExif(new ArrayCollection((array) igorw\get_in($data, ['exif'], [])));
|
||||
|
@@ -330,7 +330,7 @@ class RecordIndexer
|
||||
|
||||
foreach ($this->appbox->get_databoxes() as $databox) {
|
||||
foreach ($databox->get_statusbits() as $bit => $status) {
|
||||
$key = self::normalizeFlagKey($status['labelon']);
|
||||
$key = RecordHelper::normalizeFlagKey($status['labelon']);
|
||||
// We only add to mapping new statuses
|
||||
if (!$mapping->has($key)) {
|
||||
$mapping->add($key, 'boolean');
|
||||
@@ -341,11 +341,6 @@ class RecordIndexer
|
||||
return $mapping;
|
||||
}
|
||||
|
||||
public static function normalizeFlagKey($key)
|
||||
{
|
||||
return StringUtils::slugify($key, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspired by ESRecordSerializer
|
||||
*
|
||||
@@ -356,14 +351,12 @@ class RecordIndexer
|
||||
{
|
||||
$dateFields = $this->elasticSearchEngine->getAvailableDateFields();
|
||||
$structure = $this->getFieldsStructure();
|
||||
$databox = $this->appbox->get_databox($record['databox_id']);
|
||||
|
||||
foreach ($this->appbox->get_databoxes() as $databox) {
|
||||
foreach ($databox->get_statusbits() as $bit => $status) {
|
||||
$key = self::normalizeFlagKey($status['labelon']);
|
||||
$position = 31-$bit;
|
||||
$key = RecordHelper::normalizeFlagKey($status['labelon']);
|
||||
|
||||
$record['flags'][$key] = isset($record['bin_status']{$position}) ? (bool) $record['bin_status']{$position} : null;
|
||||
}
|
||||
$record['flags'][$key] = \databox_status::bitIsSet($record['flags_bitmask'], $bit);
|
||||
}
|
||||
|
||||
foreach ($dateFields as $field) {
|
||||
|
@@ -124,7 +124,7 @@ class RecordFetcher
|
||||
SELECT r.record_id
|
||||
, r.coll_id as collection_id
|
||||
, r.uuid
|
||||
, LPAD(BIN(r.status), 32, "0") as bin_status
|
||||
, r.status as flags_bitmask
|
||||
, r.sha256 -- TODO rename in "hash"
|
||||
, r.originalname as original_name
|
||||
, r.mime
|
||||
@@ -152,7 +152,7 @@ SQL;
|
||||
SELECT r.record_id
|
||||
, r.coll_id as collection_id
|
||||
, r.uuid
|
||||
, LPAD(BIN(r.status), 32, "0") as bin_status
|
||||
, r.status as flags_bitmask
|
||||
, r.sha256 -- TODO rename in "hash"
|
||||
, r.originalname as original_name
|
||||
, r.mime
|
||||
|
@@ -63,4 +63,9 @@ class RecordHelper
|
||||
|
||||
return $this->collectionMap;
|
||||
}
|
||||
|
||||
public static function normalizeFlagKey($key)
|
||||
{
|
||||
return StringUtils::slugify($key, '_');
|
||||
}
|
||||
}
|
||||
|
@@ -319,26 +319,9 @@ class SearchEngineOptions
|
||||
* @param array $status
|
||||
* @return SearchEngineOptions
|
||||
*/
|
||||
public function setStatus(Array $status)
|
||||
public function setStatus(array $status)
|
||||
{
|
||||
$tmp = [];
|
||||
foreach ($status as $n => $options) {
|
||||
if (count($options) > 1) {
|
||||
continue;
|
||||
}
|
||||
if (isset($options['on'])) {
|
||||
foreach ($options['on'] as $sbas_id) {
|
||||
$tmp[$n][$sbas_id] = 1;
|
||||
}
|
||||
}
|
||||
if (isset($options['off'])) {
|
||||
foreach ($options['off'] as $sbas_id) {
|
||||
$tmp[$n][$sbas_id] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->status = $tmp;
|
||||
$this->status = $status;
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -662,6 +645,7 @@ class SearchEngineOptions
|
||||
$options->allowBusinessFieldsOn($BF);
|
||||
}
|
||||
|
||||
|
||||
$status = is_array($request->get('status')) ? $request->get('status') : [];
|
||||
$fields = is_array($request->get('fields')) ? $request->get('fields') : [];
|
||||
|
||||
|
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
use Alchemy\Phrasea\Application;
|
||||
use Alchemy\Phrasea\SearchEngine\Elastic\RecordHelper;
|
||||
use MediaAlchemyst\Specification\Image as ImageSpecification;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\File\Exception\FileException;
|
||||
@@ -155,13 +156,10 @@ class databox_status
|
||||
|
||||
public static function getSearchStatus(Application $app)
|
||||
{
|
||||
$statuses = [];
|
||||
$statuses = $see_all = [];
|
||||
$databoxes = $app['acl']->get($app['authentication']->getUser())->get_granted_sbas();
|
||||
|
||||
$sbas_ids = $app['acl']->get($app['authentication']->getUser())->get_granted_sbas();
|
||||
|
||||
$see_all = [];
|
||||
|
||||
foreach ($sbas_ids as $databox) {
|
||||
foreach ($databoxes as $databox) {
|
||||
$see_all[$databox->get_sbas_id()] = false;
|
||||
|
||||
foreach ($databox->get_collections() as $collection) {
|
||||
@@ -170,6 +168,7 @@ class databox_status
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$statuses[$databox->get_sbas_id()] = $databox->get_statusbits();
|
||||
} catch (\Exception $e) {
|
||||
@@ -179,55 +178,25 @@ class databox_status
|
||||
|
||||
$stats = [];
|
||||
|
||||
foreach ($statuses as $sbas_id => $status) {
|
||||
foreach ($statuses as $databox_id => $status) {
|
||||
$canSeeAll = isset($see_all[$databox_id]) ? $see_all[$databox_id] : false;
|
||||
$canSeeThis = $app['acl']->get($app['authentication']->getUser())->has_right_on_sbas($databox_id, 'bas_modify_struct');
|
||||
|
||||
$see_this = isset($see_all[$sbas_id]) ? $see_all[$sbas_id] : false;
|
||||
|
||||
if ($app['acl']->get($app['authentication']->getUser())->has_right_on_sbas($sbas_id, 'bas_modify_struct')) {
|
||||
$see_this = true;
|
||||
}
|
||||
$canAccess = $canSeeAll || $canSeeThis;
|
||||
|
||||
foreach ($status as $bit => $props) {
|
||||
|
||||
if ($props['searchable'] == 0 && ! $see_this)
|
||||
if (!$props['searchable'] && !$canAccess) {
|
||||
continue;
|
||||
|
||||
$set = false;
|
||||
if (isset($stats[$bit])) {
|
||||
foreach ($stats[$bit] as $k => $s_desc) {
|
||||
if (mb_strtolower($s_desc['labelon']) == mb_strtolower($props['labelon'])
|
||||
&& mb_strtolower($s_desc['labeloff']) == mb_strtolower($props['labeloff'])) {
|
||||
$stats[$bit][$k]['sbas'][] = $sbas_id;
|
||||
$set = true;
|
||||
}
|
||||
}
|
||||
if (! $set) {
|
||||
$stats[$bit][] = [
|
||||
'sbas' => [$sbas_id],
|
||||
$stats[$databox_id][$bit] = array(
|
||||
'name' => RecordHelper::normalizeFlagKey($props['labelon']),
|
||||
'labeloff' => $props['labeloff'],
|
||||
'labelon' => $props['labelon'],
|
||||
'labels_on_i18n' => $props['labels_on_i18n'],
|
||||
'labels_off_i18n' => $props['labels_off_i18n'],
|
||||
'imgoff' => $props['img_off'],
|
||||
'imgon' => $props['img_on']
|
||||
];
|
||||
$set = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $set) {
|
||||
$stats[$bit] = [
|
||||
[
|
||||
'sbas' => [$sbas_id],
|
||||
'labeloff' => $props['labeloff'],
|
||||
'labelon' => $props['labelon'],
|
||||
'labels_on_i18n' => $props['labels_on_i18n'],
|
||||
'labels_off_i18n' => $props['labels_off_i18n'],
|
||||
'imgoff' => $props['img_off'],
|
||||
'imgon' => $props['img_on']
|
||||
]
|
||||
];
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,4 +605,9 @@ class databox_status
|
||||
{
|
||||
self::$_status = self::$_statuses = [];
|
||||
}
|
||||
|
||||
public static function bitIsSet($bitMask, $nthBit)
|
||||
{
|
||||
return (bool) ($bitMask & (1 << $nthBit));
|
||||
}
|
||||
}
|
||||
|
@@ -373,29 +373,29 @@
|
||||
<div class="status_filter">
|
||||
<span>{{ 'Status des documents a rechercher' | trans }}</span>
|
||||
<table style="width: 100%;">
|
||||
{% for n, stat in search_status %}
|
||||
{% for s in stat %}
|
||||
{% for sbas_id, bits in search_status %}
|
||||
{% for n, bit in bits %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if s['imgoff'] %}
|
||||
<img src="{{s['imgoff']}}" title="{{s['labels_off_i18n'][app['locale']]}}" />
|
||||
{% if bit['imgoff'] %}
|
||||
<img src="{{bit['imgoff']}}" title="{{bit['labels_off_i18n'][app['locale']]}}" />
|
||||
{% endif %}
|
||||
<label class="checkbox inline">
|
||||
<input onchange="checkFilters(true);" class="field_switch field_{{s['sbas']|join(' field_')}}"
|
||||
type="checkbox" value="{{s['sbas']|join(' field_')}}"
|
||||
n="{{n}}" name="status[{{n}}][off][]" />
|
||||
{{s['labels_off_i18n'][app['locale']]}}
|
||||
<input onchange="checkFilters(true);" class="field_switch field_{{sbas_id}}"
|
||||
type="checkbox" value="0"
|
||||
n="{{n}}" name="status[{{sbas_id}}][{{n}}]" />
|
||||
{{bit['labels_off_i18n'][app['locale']]}}
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
{% if s['imgoff'] %}
|
||||
<img src="{{s['imgon']}}" title="{{s['labels_on_i18n'][app['locale']]}}" />
|
||||
{% if bit['imgon'] %}
|
||||
<img src="{{bit['imgon']}}" title="{{bit['labels_on_i18n'][app['locale']]}}" />
|
||||
{% endif %}
|
||||
<label class="checkbox inline">
|
||||
<input onchange="checkFilters(true);" class="field_switch field_{{s['sbas']|join(' field_')}}"
|
||||
type="checkbox" value="{{s['sbas']|join(' field_')}}"
|
||||
n="{{n}}" name="status[{{n}}][on][]"/>
|
||||
{{s['labels_on_i18n'][app['locale']]}}
|
||||
<input onchange="checkFilters(true);" class="field_switch field_{{sbas_id}}"
|
||||
type="checkbox" value="1"
|
||||
n="{{n}}" name="status[{{sbas_id}}][{{n}}]"/>
|
||||
{{bit['labels_on_i18n'][app['locale']]}}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
|
Reference in New Issue
Block a user