Cleanup and improved handling of Sieve scripts

This commit is contained in:
the-djmaze 2022-08-23 10:52:23 +02:00
parent 61908dd0c0
commit 58cd2fcacb
14 changed files with 28 additions and 802 deletions

View file

@ -294,8 +294,9 @@ export class SieveScriptModel extends AbstractModel
return {
name: this.name(),
active: this.active() ? 1 : 0,
body: this.body(),
filters: this.filters.map(item => item.toJson())
body: this.body()
// body: this.allowFilters() ? this.body() : this.filtersToRaw()
// filters: this.filters.map(item => item.toJson())
};
}

View file

@ -22,8 +22,8 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
saveError: false,
errorText: '',
rawActive: false,
allowToggle: false,
script: null,
saving: false,
sieveCapabilities: '',
availableActions: '',
@ -31,8 +31,6 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
availableTests: ''
});
this.saving = false;
this.filterForDeletion = ko.observable(null).askDeleteHelper();
}
@ -48,7 +46,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
saveScript() {
let self = this,
script = self.script();
if (!self.saving/* && script.hasChanges()*/) {
if (!self.saving()/* && script.hasChanges()*/) {
this.errorText('');
self.saveError(false);
@ -62,21 +60,21 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
}
try {
parseScript(this.script().body());
parseScript(script.body());
} catch (e) {
this.errorText(e.message);
return;
}
self.saving = true;
self.saving(true);
if (self.allowToggle()) {
if (script.allowFilters()) {
script.body(script.filtersToRaw());
}
Remote.request('FiltersScriptSave',
(iError, data) => {
self.saving = false;
self.saving(false);
if (iError) {
self.saveError(true);
@ -85,6 +83,7 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
script.exists() || scripts.push(script);
script.exists(true);
script.hasChanges(false);
// this.close();
}
},
script.toJson()
@ -110,17 +109,18 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
const clonedFilter = filter.assignTo();
FilterPopupView.showModal([
clonedFilter,
() => clonedFilter.assignTo(filter),
() => {
clonedFilter.assignTo(filter);
const script = this.script();
script.hasChanges(script.body() != script.filtersToRaw());
},
true
]);
}
toggleFiltersRaw() {
let script = this.script(), notRaw = !this.rawActive();
if (notRaw) {
script.body(script.filtersToRaw());
script.hasChanges(script.hasChanges());
}
const script = this.script(), notRaw = !this.rawActive();
notRaw && script.body(script.filtersToRaw());
this.rawActive(notRaw);
}
@ -140,10 +140,8 @@ export class SieveScriptPopupView extends rl.pluginPopupView {
this.availableTests([...Object.keys(availableTests())].join(', '));
oScript = oScript || new SieveScriptModel();
let raw = !oScript.allowFilters();
this.script(oScript);
this.rawActive(raw);
this.allowToggle(!raw);
this.rawActive(!oScript.allowFilters());
this.saveError(false);
this.errorText('');

View file

@ -39,25 +39,12 @@ trait Filters
$sName = $this->GetActionParam('name', '');
$aFilters = array();
if (\RainLoop\Providers\Filters\SieveStorage::SIEVE_FILE_NAME === $sName) {
$aIncFilters = $this->GetActionParam('filters', array());
foreach ($aIncFilters as $aFilter) {
if (\is_array($aFilter)) {
$oFilter = new \RainLoop\Providers\Filters\Classes\Filter();
if ($oFilter->FromJSON($aFilter)) {
$aFilters[] = $oFilter;
}
}
}
}
if ($this->GetActionParam('active', false)) {
// $this->FiltersProvider()->ActivateScript($oAccount, $sName);
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save(
$oAccount, $sName, $aFilters, $this->GetActionParam('body', '')
$oAccount, $sName, $this->GetActionParam('body', '')
));
}

View file

@ -41,12 +41,12 @@ class Filters extends \RainLoop\Providers\AbstractProvider
}
}
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, string $sRaw) : bool
{
try
{
return $this->IsActive()
? $this->oDriver->Save($oAccount, $sScriptName, $aFilters, $sRaw)
? $this->oDriver->Save($oAccount, $sScriptName, $sRaw)
: false;
}
catch (\Throwable $oException)

View file

@ -1,227 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Classes;
class Filter implements \JsonSerializable
{
/**
* @var string
*/
private $sID;
/**
* @var bool
*/
private $bEnabled;
/**
* @var string
*/
private $sName;
/**
* @var array
*/
private $aConditions;
/**
* @var string
*/
private $sConditionsType;
/**
* @var string
*/
private $sActionType;
/**
* @var string
*/
private $sActionValue;
/**
* @var string
*/
private $sActionValueSecond;
/**
* @var string
*/
private $sActionValueThird;
/**
* @var string
*/
private $sActionValueFourth;
/**
* @var bool
*/
private $bMarkAsRead;
/**
* @var bool
*/
private $bSkipOthers;
/**
* @var bool
*/
private $bKeep;
/**
* @var bool
*/
private $bStop;
public function __construct()
{
$this->Clear();
}
public function Clear()
{
$this->sID = '';
$this->sName = '';
$this->bEnabled = true;
$this->aConditions = array();
$this->sConditionsType = \RainLoop\Providers\Filters\Enumerations\ConditionsType::ANY;
$this->sActionType = \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO;
$this->sActionValue = '';
$this->sActionValueSecond = '';
$this->sActionValueThird = '';
$this->sActionValueFourth = '';
$this->bMarkAsRead = false;
$this->bKeep = true;
$this->bStop = true;
}
public function ID() : string
{
return $this->sID;
}
public function Enabled() : bool
{
return $this->bEnabled;
}
public function Name() : string
{
return $this->sName;
}
public function Conditions() : array
{
return $this->aConditions;
}
public function ConditionsType() : string
{
return $this->sConditionsType;
}
public function ActionType() : string
{
return $this->sActionType;
}
public function ActionValue() : string
{
return $this->sActionValue;
}
public function ActionValueSecond() : string
{
return $this->sActionValueSecond;
}
public function ActionValueThird() : string
{
return $this->sActionValueThird;
}
public function ActionValueFourth() : string
{
return $this->sActionValueFourth;
}
public function MarkAsRead() : bool
{
return $this->bMarkAsRead;
}
public function Stop() : bool
{
return $this->bStop;
}
public function Keep() : bool
{
return $this->bKeep;
}
public function unserializeFromJson(string $sFilterJson)
{
$aFilterJson = \json_decode(\trim($sFilterJson), true);
if (\is_array($aFilterJson))
{
return $this->FromJSON($aFilterJson);
}
return false;
}
public function FromJSON(array $aFilter) : bool
{
$this->sID = $aFilter['ID'] ?? '';
$this->sName = $aFilter['Name'] ?? '';
$this->bEnabled = !isset($aFilter['Enabled']) || !empty($aFilter['Enabled']);
$this->sConditionsType = $aFilter['ConditionsType']
?? \RainLoop\Providers\Filters\Enumerations\ConditionsType::ANY;
$this->sActionType = $aFilter['ActionType']
?? \RainLoop\Providers\Filters\Enumerations\ActionType::MOVE_TO;
$this->sActionValue = $aFilter['ActionValue'] ?? '';
$this->sActionValueSecond = $aFilter['ActionValueSecond'] ?? '';
$this->sActionValueThird = $aFilter['ActionValueThird'] ?? '';
$this->sActionValueFourth = $aFilter['ActionValueFourth'] ?? '';
$this->bKeep = !isset($aFilter['Keep']) || !empty($aFilter['Keep']);
$this->bStop = !isset($aFilter['Stop']) || !empty($aFilter['Stop']);
$this->bMarkAsRead = !isset($aFilter['MarkAsRead']) || !empty($aFilter['MarkAsRead']);
$this->aConditions = empty($aFilter['Conditions']) ? array() : FilterCondition::CollectionFromJSON($aFilter['Conditions']);
return !empty($this->aConditions);
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'@Object' => 'Object/Filter',
'ID' => $this->ID(),
'Enabled' => $this->Enabled(),
'Name' => $this->Name(),
'Conditions' => $this->Conditions(),
'ConditionsType' => $this->ConditionsType(),
'ActionType' => $this->ActionType(),
'ActionValue' => $this->ActionValue(),
'ActionValueSecond' => $this->ActionValueSecond(),
'ActionValueThird' => $this->ActionValueThird(),
'ActionValueFourth' => $this->ActionValueFourth(),
'Keep' => $this->Keep(),
'Stop' => $this->Stop(),
'MarkAsRead' => $this->MarkAsRead()
);
}
}

View file

@ -1,88 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Classes;
class FilterCondition implements \JsonSerializable
{
/**
* @var string
*/
private $sField = '';
/**
* @var string
*/
private $sType = '';
/**
* @var string
*/
private $sValue = '';
/**
* @var string
*/
private $sValueSecond = '';
public function Field() : string
{
return $this->sField;
}
public function Type() : string
{
return $this->sType;
}
public function Value() : string
{
return $this->sValue;
}
public function ValueSecond() : string
{
return $this->sValueSecond;
}
public function FromJSON(array $aData) : bool
{
if (empty($aData['Field']) || empty($aData['Type']) || empty($aData['Value'])) {
return false;
}
$this->sField = $aData['Field'];
$this->sType = $aData['Type'];
$this->sValue = (string) $aData['Value'];
$this->sValueSecond = isset($aData['ValueSecond']) ? (string) $aData['ValueSecond'] : '';
return true;
}
public static function CollectionFromJSON(array $aCollection) : array
{
$aResult = array();
foreach ($aCollection as $aItem)
{
if (\is_array($aItem) && \count($aItem))
{
$oItem = new \RainLoop\Providers\Filters\Classes\FilterCondition();
if ($oItem->FromJSON($aItem))
{
$aResult[] = $oItem;
}
}
}
return $aResult;
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'@Object' => 'Object/FilterCondition',
'Field' => $this->Field(),
'Type' => $this->Type(),
'Value' => $this->Value(),
'ValueSecond' => $this->ValueSecond()
);
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Enumerations;
class ActionType
{
const NONE = 'None';
const MOVE_TO = 'MoveTo';
const DISCARD = 'Discard';
const VACATION = 'Vacation';
const REJECT = 'Reject';
const FORWARD = 'Forward';
}

View file

@ -1,14 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Enumerations;
class ConditionField
{
const
FROM = 'From',
RECIPIENT = 'Recipient',
SUBJECT = 'Subject',
HEADER = 'Header',
BODY = 'Body',
SIZE = 'Size';
}

View file

@ -1,17 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Enumerations;
class ConditionType
{
const
EQUAL_TO = 'EqualTo',
NOT_EQUAL_TO = 'NotEqualTo',
CONTAINS = 'Contains',
NOT_CONTAINS = 'NotContains',
REGEX = 'Regex',
OVER = 'Over',
UNDER = 'Under',
TEXT = 'Text',
RAW = 'Raw';
}

View file

@ -1,9 +0,0 @@
<?php
namespace RainLoop\Providers\Filters\Enumerations;
class ConditionsType
{
const ALL = 'All';
const ANY = 'Any';
}

View file

@ -6,7 +6,7 @@ interface FiltersInterface
{
public function Load(\RainLoop\Model\Account $oAccount) : array;
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool;
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, string $sRaw) : bool;
public function Activate(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool;

View file

@ -1,383 +0,0 @@
<?php
namespace RainLoop\Providers\Filters;
class Sieve
{
const NEW_LINE = "\r\n";
public static function collectionToFileString(array $aFilters) : string
{
$sNL = static::NEW_LINE;
$aCapa = array();
$aParts = [
'# This is SnappyMail sieve script.',
'# Please don\'t change anything here.',
'# RAINLOOP:SIEVE',
''
];
foreach ($aFilters as /* @var $oItem \RainLoop\Providers\Filters\Classes\Filter */ $oItem)
{
$aParts[] = \implode($sNL, [
'/*',
'BEGIN:FILTER:'.$oItem->ID(),
'BEGIN:HEADER',
\chunk_split(\base64_encode(\json_encode($oItem)), 74, $sNL).'END:HEADER',
'*/',
$oItem->Enabled() ? '' : '/* @Filter is disabled ',
static::filterToSieveScript($oItem, $aCapa),
$oItem->Enabled() ? '' : '*/',
'/* END:FILTER */',
''
]);
}
$aCapa = \array_keys($aCapa);
$sCapa = \count($aCapa)
? $sNL . 'require ' . \str_replace('","', '", "', \json_encode($aCapa)).';' . $sNL
: '';
return $sCapa . $sNL . \implode($sNL, $aParts);
}
public static function fileStringToCollection(string $sFileString) : array
{
$aResult = array();
if (!empty($sFileString) && false !== \strpos($sFileString, 'RAINLOOP:SIEVE'))
{
$aMatch = array();
if (\preg_match_all('/BEGIN:FILTER(.+?)BEGIN:HEADER(.+?)END:HEADER/s', $sFileString, $aMatch) &&
isset($aMatch[2]) && \is_array($aMatch[2]))
{
foreach ($aMatch[2] as $sEncodedLine)
{
if (!empty($sEncodedLine))
{
$sDecodedLine = \base64_decode(\preg_replace('/\\s+/s', '', $sEncodedLine));
if (!empty($sDecodedLine))
{
$oItem = new Classes\Filter();
if ($oItem && $oItem->unserializeFromJson($sDecodedLine))
{
$aResult[] = $oItem;
}
}
}
}
}
}
return $aResult;
}
private static function conditionToSieveScript(Classes\FilterCondition $oCondition, array &$aCapa) : string
{
$sResult = '';
$sTypeWord = '';
$bTrue = true;
$sValue = \trim($oCondition->Value());
$sValueSecond = \trim($oCondition->ValueSecond());
if (\strlen($sValue) ||
(\strlen($sValue) && \strlen($sValueSecond) &&
Enumerations\ConditionField::HEADER === $oCondition->Field()))
{
switch ($oCondition->Type())
{
case Enumerations\ConditionType::TEXT:
case Enumerations\ConditionType::RAW:
case Enumerations\ConditionType::OVER:
case Enumerations\ConditionType::UNDER:
$sTypeWord = ':' . \strtolower($oCondition->Type());
break;
case Enumerations\ConditionType::NOT_EQUAL_TO:
$sResult .= 'not ';
case Enumerations\ConditionType::EQUAL_TO:
$sTypeWord = ':is';
break;
case Enumerations\ConditionType::NOT_CONTAINS:
$sResult .= 'not ';
case Enumerations\ConditionType::CONTAINS:
$sTypeWord = ':contains';
break;
case Enumerations\ConditionType::REGEX:
$sTypeWord = ':regex';
$aCapa['regex'] = true;
break;
default:
$bTrue = false;
$sResult = '/* @Error: unknown type value */ false';
break;
}
switch ($oCondition->Field())
{
case Enumerations\ConditionField::FROM:
$sResult .= 'header '.$sTypeWord.' ["From"]';
break;
case Enumerations\ConditionField::RECIPIENT:
$sResult .= 'header '.$sTypeWord.' ["To", "CC"]';
break;
case Enumerations\ConditionField::SUBJECT:
$sResult .= 'header '.$sTypeWord.' ["Subject"]';
break;
case Enumerations\ConditionField::HEADER:
$sResult .= 'header '.$sTypeWord.' ["'.static::quote($sValueSecond).'"]';
break;
case Enumerations\ConditionField::BODY:
// :text :raw :content
$sResult .= 'body '.$sTypeWord.' :contains';
$aCapa['body'] = true;
break;
case Enumerations\ConditionField::SIZE:
$sResult .= 'size '.$sTypeWord;
break;
default:
$bTrue = false;
$sResult = '/* @Error: unknown field value */ false';
break;
}
if ($bTrue)
{
if (\in_array($oCondition->Field(), array(
Enumerations\ConditionField::FROM,
Enumerations\ConditionField::RECIPIENT
)) && false !== \strpos($sValue, ','))
{
$aValue = \array_map(function ($sValue) use ($self) {
return '"'.static::quote(\trim($sValue)).'"';
}, \explode(',', $sValue));
$sResult .= ' ['.\trim(\implode(', ', $aValue)).']';
}
else if (Enumerations\ConditionField::SIZE === $oCondition->Field())
{
$sResult .= ' '.static::quote($sValue);
}
else
{
$sResult .= ' "'.static::quote($sValue).'"';
}
$sResult = \MailSo\Base\Utils::StripSpaces($sResult);
}
}
else
{
$sResult = '/* @Error: empty condition value */ false';
}
return $sResult;
}
private static function filterToSieveScript(Classes\Filter $oFilter, array &$aCapa) : string
{
$sNL = static::NEW_LINE;
$sTab = ' ';
$bBlock = true;
$aResult = array();
// Conditions
$aConditions = $oFilter->Conditions();
if (1 < \count($aConditions))
{
if (Enumerations\ConditionsType::ANY ===
$oFilter->ConditionsType())
{
$aResult[] = 'if anyof(';
$bTrim = false;
foreach ($aConditions as $oCond)
{
$bTrim = true;
$sCons = static::conditionToSieveScript($oCond, $aCapa);
if (!empty($sCons))
{
$aResult[] = $sTab.$sCons.',';
}
}
if ($bTrim)
{
$aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ',');
}
$aResult[] = ')';
}
else
{
$aResult[] = 'if allof(';
foreach ($aConditions as $oCond)
{
$aResult[] = $sTab . static::conditionToSieveScript($oCond, $aCapa) . ',';
}
$aResult[\count($aResult) - 1] = \rtrim($aResult[\count($aResult) - 1], ',');
$aResult[] = ')';
}
}
else if (1 === \count($aConditions))
{
$aResult[] = 'if ' . static::conditionToSieveScript($aConditions[0], $aCapa);
}
else
{
$bBlock = false;
}
// actions
if ($bBlock)
{
$aResult[] = '{';
}
else
{
$sTab = '';
}
if ($oFilter->MarkAsRead() && \in_array($oFilter->ActionType(), array(
Enumerations\ActionType::NONE,
Enumerations\ActionType::MOVE_TO,
Enumerations\ActionType::FORWARD
)))
{
$aCapa['imap4flags'] = true;
$aResult[] = $sTab.'addflag "\\\\Seen";';
}
switch ($oFilter->ActionType())
{
case Enumerations\ActionType::NONE:
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
break;
case Enumerations\ActionType::DISCARD:
$aResult[] = $sTab.'discard;';
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
break;
case Enumerations\ActionType::VACATION:
$sValue = \trim($oFilter->ActionValue());
$sValueSecond = \trim($oFilter->ActionValueSecond());
$sValueThird = \trim($oFilter->ActionValueThird());
$sValueFourth = \trim($oFilter->ActionValueFourth());
if (\strlen($sValue))
{
$aCapa['vacation'] = true;
$iDays = 1;
$sSubject = '';
if (\strlen($sValueSecond))
{
$sSubject = ':subject "'.
static::quote(\MailSo\Base\Utils::StripSpaces($sValueSecond)).'" ';
}
if (\strlen($sValueThird) && \is_numeric($sValueThird) && 1 < (int) $sValueThird)
{
$iDays = (int) $sValueThird;
}
$sAddresses = '';
if (\strlen($sValueFourth))
{
$aAddresses = \explode(',', $sValueFourth);
$aAddresses = \array_filter(\array_map(function ($sEmail) use ($self) {
$sEmail = \trim($sEmail);
return \strlen($sEmail) ? '"'.static::quote($sEmail).'"' : '';
}, $aAddresses), 'strlen');
if (\count($aAddresses))
{
$sAddresses = ':addresses ['.\implode(', ', $aAddresses).'] ';
}
}
$aResult[] = $sTab.'vacation :days '.$iDays.' '.$sAddresses.$sSubject.'"'.static::quote($sValue).'";';
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
}
else
{
$aResult[] = $sTab.'# @Error (vacation): empty action value';
}
break;
case Enumerations\ActionType::REJECT:
$sValue = \trim($oFilter->ActionValue());
if (\strlen($sValue))
{
$aCapa['reject'] = true;
$aResult[] = $sTab.'reject "'.static::quote($sValue).'";';
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
}
else
{
$aResult[] = $sTab.'# @Error (reject): empty action value';
}
break;
case Enumerations\ActionType::FORWARD:
$sValue = $oFilter->ActionValue();
if (\strlen($sValue))
{
if ($oFilter->Keep())
{
$aCapa['fileinto'] = true;
$aResult[] = $sTab.'fileinto "INBOX";';
}
$aResult[] = $sTab.'redirect "'.static::quote($sValue).'";';
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
}
else
{
$aResult[] = $sTab.'# @Error (redirect): empty action value';
}
break;
case Enumerations\ActionType::MOVE_TO:
$sFolderName = $oFilter->ActionValue();
if (\strlen($sFolderName))
{
$aCapa['fileinto'] = true;
$aResult[] = $sTab.'fileinto "'.static::quote($sFolderName).'";';
if ($oFilter->Stop())
{
$aResult[] = $sTab.'stop;';
}
}
else
{
$aResult[] = $sTab.'# @Error (fileinto): empty action value';
}
break;
}
if ($bBlock)
{
$aResult[] = '}';
}
return \implode($sNL, $aResult);
}
private static function quote(string $sValue) : string
{
return \str_replace(array('\\', '"'), array('\\\\', '\\"'), \trim($sValue));
}
}

View file

@ -81,22 +81,11 @@ class SieveStorage implements FiltersInterface
);
}
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, string $sRaw) : bool
{
if ($aFilters && !$sRaw) {
$sRaw = Sieve::collectionToFileString($aFilters);
}
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
if (empty($sRaw)) {
$aList = $oSieveClient->ListScripts();
if (isset($aList[$sScriptName])) {
$oSieveClient->DeleteScript($sScriptName);
}
} else {
$oSieveClient->PutScript($sScriptName, $sRaw);
}
$oSieveClient->Disconnect();
$oSieveClient->PutScript($sScriptName, $sRaw);
return true;
}
return false;
@ -129,7 +118,9 @@ class SieveStorage implements FiltersInterface
{
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
$oSieveClient->DeleteScript(\trim($sScriptName));
if (isset($oSieveClient->ListScripts()[$sScriptName])) {
$oSieveClient->DeleteScript(\trim($sScriptName));
}
return true;
}
return false;

View file

@ -70,7 +70,7 @@
</div>
</div>
<footer>
<a class="btn" data-bind="visible: $root.allowToggle, click: function() { $root.toggleFiltersRaw(); }, css: {'active': $root.rawActive }" data-i18n="[title]POPUPS_SIEVE_SCRIPT/BUTTON_RAW_SCRIPT">
<a class="btn" data-bind="visible: allowFilters(), click: function() { $root.toggleFiltersRaw(); }, css: {'active': $root.rawActive }" data-i18n="[title]POPUPS_SIEVE_SCRIPT/BUTTON_RAW_SCRIPT">
<i class="icon-file-code"></i>
</a>
<!--