3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-23 11:12:44 +01:00
ergo/irc/modes.go

696 lines
18 KiB
Go
Raw Normal View History

// Copyright (c) 2012-2014 Jeremy Latt
// Copyright (c) 2014-2015 Edmund Huber
2017-03-12 23:08:18 +01:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"strconv"
"strings"
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircmsg"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/sno"
"github.com/tidwall/buntdb"
)
2017-03-12 23:08:18 +01:00
// ModeOp is an operation performed with modes
type ModeOp rune
2017-03-12 23:08:18 +01:00
func (op ModeOp) String() string {
return string(op)
}
2017-03-12 23:08:18 +01:00
const (
// Add is used when adding the given key.
Add ModeOp = '+'
// List is used when listing modes (for instance, listing the current bans on a channel).
List ModeOp = '='
// Remove is used when taking away the given key.
2017-03-12 23:08:18 +01:00
Remove ModeOp = '-'
)
2017-03-12 23:08:18 +01:00
// Mode represents a user/channel/server mode
type Mode rune
2017-03-12 23:08:18 +01:00
func (mode Mode) String() string {
return string(mode)
}
2017-03-12 23:08:18 +01:00
// ModeChange is a single mode changing
type ModeChange struct {
mode Mode
op ModeOp
arg string
}
2017-03-12 23:08:18 +01:00
func (change *ModeChange) String() (str string) {
if (change.op == Add) || (change.op == Remove) {
str = change.op.String()
}
str += change.mode.String()
if change.arg != "" {
str += " " + change.arg
}
return
}
2017-03-12 23:08:18 +01:00
// ModeChanges are a collection of 'ModeChange's
type ModeChanges []ModeChange
2017-03-12 23:08:18 +01:00
func (changes ModeChanges) String() string {
if len(changes) == 0 {
return ""
}
op := changes[0].op
str := changes[0].op.String()
for _, change := range changes {
if change.op != op {
op = change.op
str += change.op.String()
}
str += change.mode.String()
}
for _, change := range changes {
if change.arg == "" {
continue
}
str += " " + change.arg
}
return str
}
2017-03-12 23:08:18 +01:00
// Modes is just a raw list of modes
type Modes []Mode
2017-03-12 23:08:18 +01:00
func (modes Modes) String() string {
strs := make([]string, len(modes))
for index, mode := range modes {
strs[index] = mode.String()
}
return strings.Join(strs, "")
}
2017-03-12 23:08:18 +01:00
// User Modes
const (
2017-03-12 23:08:18 +01:00
Away Mode = 'a'
Invisible Mode = 'i'
LocalOperator Mode = 'O'
Operator Mode = 'o'
Restricted Mode = 'r'
RegisteredOnly Mode = 'R'
2017-05-08 01:15:16 +02:00
ServerNotice Mode = 's'
2017-03-12 23:08:18 +01:00
TLS Mode = 'Z'
UserRoleplaying Mode = 'E'
WallOps Mode = 'w'
)
var (
// SupportedUserModes are the user modes that we actually support (modifying).
2017-03-12 23:08:18 +01:00
SupportedUserModes = Modes{
Away, Invisible, Operator, RegisteredOnly, ServerNotice, UserRoleplaying,
}
2016-06-19 13:59:18 +02:00
// supportedUserModesString acts as a cache for when we introduce users
supportedUserModesString = SupportedUserModes.String()
)
2017-03-12 23:08:18 +01:00
// Channel Modes
const (
2017-03-12 23:08:18 +01:00
BanMask Mode = 'b' // arg
ChanRoleplaying Mode = 'E' // flag
ExceptMask Mode = 'e' // arg
InviteMask Mode = 'I' // arg
InviteOnly Mode = 'i' // flag
Key Mode = 'k' // flag arg
Moderated Mode = 'm' // flag
NoOutside Mode = 'n' // flag
OpOnlyTopic Mode = 't' // flag
// RegisteredOnly mode is reused here from umode definition
Secret Mode = 's' // flag
UserLimit Mode = 'l' // flag arg
)
var (
2017-03-12 23:08:18 +01:00
ChannelFounder Mode = 'q' // arg
ChannelAdmin Mode = 'a' // arg
ChannelOperator Mode = 'o' // arg
Halfop Mode = 'h' // arg
Voice Mode = 'v' // arg
2016-10-22 16:45:51 +02:00
// SupportedChannelModes are the channel modes that we support.
2017-03-12 23:08:18 +01:00
SupportedChannelModes = Modes{
BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key,
Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, Secret, UserLimit,
}
2016-06-19 13:59:18 +02:00
// supportedChannelModesString acts as a cache for when we introduce users
supportedChannelModesString = SupportedChannelModes.String()
// DefaultChannelModes are enabled on brand new channels when they're created.
// this can be overridden in the `channels` config, with the `default-modes` key
2017-03-12 23:08:18 +01:00
DefaultChannelModes = Modes{
2016-04-21 11:29:50 +02:00
NoOutside, OpOnlyTopic,
}
// ChannelPrivModes holds the list of modes that are privileged, ie founder/op/halfop, in order.
// voice is not in this list because it cannot perform channel operator actions.
2017-03-12 23:08:18 +01:00
ChannelPrivModes = Modes{
ChannelFounder, ChannelAdmin, ChannelOperator, Halfop,
}
2017-03-12 23:08:18 +01:00
ChannelModePrefixes = map[Mode]string{
ChannelFounder: "~",
ChannelAdmin: "&",
ChannelOperator: "@",
Halfop: "%",
Voice: "+",
}
)
2017-03-12 23:08:18 +01:00
//
// channel membership prefixes
//
2016-10-22 16:45:51 +02:00
// SplitChannelMembershipPrefixes takes a target and returns the prefixes on it, then the name.
func SplitChannelMembershipPrefixes(target string) (prefixes string, name string) {
name = target
for {
if len(name) > 0 && strings.Contains("~&@%+", string(name[0])) {
2016-10-22 16:45:51 +02:00
prefixes += string(name[0])
name = name[1:]
} else {
break
}
}
return prefixes, name
}
// GetLowestChannelModePrefix returns the lowest channel prefix mode out of the given prefixes.
2017-03-24 03:23:21 +01:00
func GetLowestChannelModePrefix(prefixes string) *Mode {
var lowest *Mode
2016-10-22 16:45:51 +02:00
if strings.Contains(prefixes, "+") {
lowest = &Voice
} else {
for i, mode := range ChannelPrivModes {
if strings.Contains(prefixes, ChannelModePrefixes[mode]) {
lowest = &ChannelPrivModes[i]
}
}
}
return lowest
}
//
// commands
//
// MODE <target> [<modestring> [<mode arguments>...]]
func modeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
_, errChan := CasefoldChannel(msg.Params[0])
2016-10-16 13:28:59 +02:00
if errChan == nil {
2016-06-22 13:35:26 +02:00
return cmodeHandler(server, client, msg)
}
2017-03-12 23:08:18 +01:00
return umodeHandler(server, client, msg)
}
2017-05-08 01:15:16 +02:00
// ParseUserModeChanges returns the valid changes, and the list of unknown chars.
func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) {
changes := make(ModeChanges, 0)
unknown := make(map[rune]bool)
op := List
2017-05-08 01:15:16 +02:00
if 0 < len(params) {
modeArg := params[0]
skipArgs := 1
for _, mode := range modeArg {
if mode == '-' || mode == '+' {
op = ModeOp(mode)
continue
}
change := ModeChange{
mode: Mode(mode),
op: op,
}
// put arg into modechange if needed
switch Mode(mode) {
case ServerNotice:
// always require arg
if len(params) > skipArgs {
change.arg = params[skipArgs]
skipArgs++
} else {
continue
}
}
var isKnown bool
for _, supportedMode := range SupportedUserModes {
if rune(supportedMode) == mode {
isKnown = true
break
}
}
if !isKnown {
unknown[mode] = true
continue
}
changes = append(changes, change)
}
}
return changes, unknown
}
2017-03-24 03:23:21 +01:00
// applyUserModeChanges applies the given changes, and returns the applied changes.
2017-05-08 01:15:16 +02:00
func (client *Client) applyUserModeChanges(force bool, changes ModeChanges) ModeChanges {
2017-03-12 23:08:18 +01:00
applied := make(ModeChanges, 0)
for _, change := range changes {
switch change.mode {
case Invisible, WallOps, UserRoleplaying, Operator, LocalOperator, RegisteredOnly:
2017-03-12 23:08:18 +01:00
switch change.op {
case Add:
2017-05-08 01:15:16 +02:00
if !force && (change.mode == Operator || change.mode == LocalOperator) {
continue
}
2017-03-12 23:08:18 +01:00
if client.flags[change.mode] {
continue
}
client.flags[change.mode] = true
applied = append(applied, change)
case Remove:
if !client.flags[change.mode] {
continue
}
delete(client.flags, change.mode)
applied = append(applied, change)
}
2017-05-08 01:15:16 +02:00
case ServerNotice:
if !client.flags[Operator] {
continue
}
var masks []sno.Mask
if change.op == Add || change.op == Remove {
for _, char := range change.arg {
masks = append(masks, sno.Mask(char))
2017-03-12 23:08:18 +01:00
}
2017-05-08 01:15:16 +02:00
}
if change.op == Add {
client.server.snomasks.AddMasks(client, masks...)
applied = append(applied, change)
} else if change.op == Remove {
client.server.snomasks.RemoveMasks(client, masks...)
2017-03-12 23:08:18 +01:00
applied = append(applied, change)
}
}
// can't do anything to TLS mode
}
// return the changes we could actually apply
return applied
}
// MODE <target> [<modestring> [<mode arguments>...]]
func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
nickname, err := CasefoldName(msg.Params[0])
target := server.clients.Get(nickname)
if err != nil || target == nil {
if len(msg.Params[0]) > 0 {
client.Send(nil, server.name, ERR_NOSUCHNICK, client.nick, msg.Params[0], "No such nick")
}
return false
}
2017-10-24 00:01:28 +02:00
targetNick := target.getNick()
hasPrivs := client == target || msg.Command == "SAMODE"
if !hasPrivs {
if len(msg.Params) > 1 {
client.Send(nil, server.name, ERR_USERSDONTMATCH, client.nick, "Can't change modes for other users")
} else {
client.Send(nil, server.name, ERR_USERSDONTMATCH, client.nick, "Can't view modes for other users")
}
return false
}
2017-05-08 01:15:16 +02:00
// applied mode changes
2016-06-22 13:35:26 +02:00
applied := make(ModeChanges, 0)
2017-05-08 01:15:16 +02:00
if 1 < len(msg.Params) {
// parse out real mode changes
params := msg.Params[1:]
changes, unknown := ParseUserModeChanges(params...)
2017-05-08 01:15:16 +02:00
// alert for unknown mode changes
for char := range unknown {
client.Send(nil, server.name, ERR_UNKNOWNMODE, client.nick, string(char), "is an unknown mode character to me")
}
if len(unknown) == 1 && len(changes) == 0 {
return false
}
2017-05-08 01:15:16 +02:00
// apply mode changes
applied = target.applyUserModeChanges(msg.Command == "SAMODE", changes)
}
2016-06-28 17:09:07 +02:00
if len(applied) > 0 {
client.Send(nil, client.nickMaskString, "MODE", targetNick, applied.String())
} else if hasPrivs {
client.Send(nil, target.nickMaskString, RPL_UMODEIS, targetNick, target.ModeString())
2017-05-08 01:15:16 +02:00
if client.flags[LocalOperator] || client.flags[Operator] {
masks := server.snomasks.String(client)
if 0 < len(masks) {
client.Send(nil, target.nickMaskString, RPL_SNOMASKIS, targetNick, masks, "Server notice masks")
2017-05-08 01:15:16 +02:00
}
}
}
return false
}
// ParseDefaultChannelModes parses the `default-modes` line of the config
func ParseDefaultChannelModes(config *Config) Modes {
if config.Channels.DefaultModes == nil {
// not present in config, fall back to compile-time default
return DefaultChannelModes
}
modeChangeStrings := strings.Split(strings.TrimSpace(*config.Channels.DefaultModes), " ")
modeChanges, _ := ParseChannelModeChanges(modeChangeStrings...)
defaultChannelModes := make(Modes, 0)
for _, modeChange := range modeChanges {
if modeChange.op == Add {
defaultChannelModes = append(defaultChannelModes, modeChange.mode)
}
}
return defaultChannelModes
}
2017-03-24 03:23:21 +01:00
// ParseChannelModeChanges returns the valid changes, and the list of unknown chars.
func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) {
changes := make(ModeChanges, 0)
unknown := make(map[rune]bool)
2016-06-22 13:35:26 +02:00
op := List
if 0 < len(params) {
2017-03-24 03:23:21 +01:00
modeArg := params[0]
skipArgs := 1
2016-06-22 13:35:26 +02:00
for _, mode := range modeArg {
if mode == '-' || mode == '+' {
op = ModeOp(mode)
continue
}
2017-03-24 03:23:21 +01:00
change := ModeChange{
mode: Mode(mode),
2016-06-22 13:35:26 +02:00
op: op,
}
// put arg into modechange if needed
2017-03-24 03:23:21 +01:00
switch Mode(mode) {
2016-06-22 13:35:26 +02:00
case BanMask, ExceptMask, InviteMask:
2017-03-24 03:23:21 +01:00
if len(params) > skipArgs {
change.arg = params[skipArgs]
2016-09-14 11:48:47 +02:00
skipArgs++
2016-06-22 13:35:26 +02:00
} else {
change.op = List
}
2016-09-14 11:48:47 +02:00
case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice:
2017-03-24 03:23:21 +01:00
if len(params) > skipArgs {
change.arg = params[skipArgs]
2016-09-14 11:48:47 +02:00
skipArgs++
2016-06-22 13:35:26 +02:00
} else {
continue
}
2016-09-14 11:48:47 +02:00
case Key, UserLimit:
// don't require value when removing
if change.op == Add {
2017-03-24 03:23:21 +01:00
if len(params) > skipArgs {
change.arg = params[skipArgs]
2016-09-14 11:48:47 +02:00
skipArgs++
} else {
continue
}
}
}
var isKnown bool
for _, supportedMode := range SupportedChannelModes {
if rune(supportedMode) == mode {
isKnown = true
break
}
}
for _, supportedMode := range ChannelPrivModes {
if rune(supportedMode) == mode {
isKnown = true
break
}
}
2017-06-29 17:14:38 +02:00
if mode == rune(Voice) {
isKnown = true
}
if !isKnown {
2017-03-24 03:23:21 +01:00
unknown[mode] = true
2017-05-08 01:15:16 +02:00
continue
2016-06-22 13:35:26 +02:00
}
2017-03-24 03:23:21 +01:00
changes = append(changes, change)
2016-06-22 13:35:26 +02:00
}
2017-03-24 03:23:21 +01:00
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
return changes, unknown
}
2016-10-23 16:50:18 +02:00
2017-03-24 03:23:21 +01:00
// ApplyChannelModeChanges applies a given set of mode changes.
2017-10-23 01:50:16 +02:00
func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes ModeChanges) ModeChanges {
2017-03-24 03:23:21 +01:00
// so we only output one warning for each list type when full
listFullWarned := make(map[Mode]bool)
2017-10-23 01:50:16 +02:00
clientIsOp := channel.ClientIsAtLeast(client, ChannelOperator)
2017-03-24 03:23:21 +01:00
var alreadySentPrivError bool
applied := make(ModeChanges, 0)
for _, change := range changes {
// chan priv modes are checked specially so ignore them
// means regular users can't view ban/except lists... but I'm not worried about that
if isSamode && ChannelModePrefixes[change.mode] == "" && !clientIsOp {
if !alreadySentPrivError {
alreadySentPrivError = true
client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator")
}
2017-03-24 03:23:21 +01:00
continue
}
2017-03-24 03:23:21 +01:00
switch change.mode {
case BanMask, ExceptMask, InviteMask:
mask := change.arg
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
if (change.op == List) || (mask == "") {
channel.ShowMaskList(client, change.mode)
continue
}
// confirm mask looks valid
mask, err := Casefold(mask)
if err != nil {
continue
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
switch change.op {
case Add:
2017-10-23 01:50:16 +02:00
if channel.lists[change.mode].Length() >= client.server.getLimits().ChanListModes {
2017-03-24 03:23:21 +01:00
if !listFullWarned[change.mode] {
2017-10-23 01:50:16 +02:00
client.Send(nil, client.server.name, ERR_BANLISTFULL, client.getNick(), channel.Name(), change.mode.String(), "Channel list is full")
2017-03-24 03:23:21 +01:00
listFullWarned[change.mode] = true
}
continue
}
2017-10-23 01:50:16 +02:00
channel.lists[change.mode].Add(mask)
2017-03-24 03:23:21 +01:00
applied = append(applied, change)
2016-10-23 16:50:18 +02:00
2017-03-24 03:23:21 +01:00
case Remove:
2017-10-23 01:50:16 +02:00
channel.lists[change.mode].Remove(mask)
2017-03-24 03:23:21 +01:00
applied = append(applied, change)
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
case UserLimit:
switch change.op {
case Add:
val, err := strconv.ParseUint(change.arg, 10, 64)
if err == nil {
2017-10-23 01:50:16 +02:00
channel.setUserLimit(val)
2016-06-22 13:35:26 +02:00
applied = append(applied, change)
}
2017-03-24 03:23:21 +01:00
case Remove:
2017-10-23 01:50:16 +02:00
channel.setUserLimit(0)
2017-03-24 03:23:21 +01:00
applied = append(applied, change)
}
2016-09-14 11:48:47 +02:00
2017-03-24 03:23:21 +01:00
case Key:
switch change.op {
case Add:
2017-10-23 01:50:16 +02:00
channel.setKey(change.arg)
2016-09-14 11:48:47 +02:00
2017-03-24 03:23:21 +01:00
case Remove:
2017-10-23 01:50:16 +02:00
channel.setKey("")
2017-03-24 03:23:21 +01:00
}
applied = append(applied, change)
2016-09-14 11:48:47 +02:00
case InviteOnly, Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, Secret, ChanRoleplaying:
2017-10-23 01:50:16 +02:00
if change.op == List {
continue
}
2016-09-14 11:48:47 +02:00
2017-10-23 01:50:16 +02:00
already := channel.setMode(change.mode, change.op == Add)
if !already {
2017-03-24 03:23:21 +01:00
applied = append(applied, change)
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice:
2017-10-23 01:50:16 +02:00
if change.op == List {
continue
}
2017-03-24 03:23:21 +01:00
// make sure client has privs to edit the given prefix
hasPrivs := isSamode
2016-06-22 13:35:26 +02:00
2017-10-23 01:50:16 +02:00
// Admins can't give other people Admin or remove it from others,
// standard for that channel mode, we worry about this later
if !hasPrivs && change.mode != ChannelAdmin {
hasPrivs = channel.ClientIsAtLeast(client, change.mode)
2017-03-24 03:23:21 +01:00
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
casefoldedName, err := CasefoldName(change.arg)
if err != nil {
continue
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
if !hasPrivs {
if change.op == Remove && casefoldedName == client.nickCasefolded {
// success!
} else {
if !alreadySentPrivError {
alreadySentPrivError = true
client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator")
2016-06-22 13:35:26 +02:00
}
2017-03-24 03:23:21 +01:00
continue
2016-06-22 13:35:26 +02:00
}
2017-03-24 03:23:21 +01:00
}
2016-06-22 13:35:26 +02:00
2017-03-24 03:23:21 +01:00
change := channel.applyModeMemberNoMutex(client, change.mode, change.op, change.arg)
if change != nil {
applied = append(applied, *change)
2016-06-22 13:35:26 +02:00
}
}
}
2017-03-24 03:23:21 +01:00
return applied
}
// MODE <target> [<modestring> [<mode arguments>...]]
func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
channelName, err := CasefoldChannel(msg.Params[0])
channel := server.channels.Get(channelName)
if err != nil || channel == nil {
client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, msg.Params[0], "No such channel")
return false
}
// applied mode changes
applied := make(ModeChanges, 0)
if 1 < len(msg.Params) {
// parse out real mode changes
params := msg.Params[1:]
changes, unknown := ParseChannelModeChanges(params...)
// alert for unknown mode changes
for char := range unknown {
client.Send(nil, server.name, ERR_UNKNOWNMODE, client.nick, string(char), "is an unknown mode character to me")
}
if len(unknown) == 1 && len(changes) == 0 {
return false
}
// apply mode changes
2017-10-23 01:50:16 +02:00
applied = channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes)
2017-03-24 03:23:21 +01:00
}
// save changes to banlist/exceptlist/invexlist
var banlistUpdated, exceptlistUpdated, invexlistUpdated bool
for _, change := range applied {
if change.mode == BanMask {
banlistUpdated = true
} else if change.mode == ExceptMask {
exceptlistUpdated = true
} else if change.mode == InviteMask {
invexlistUpdated = true
}
}
server.registeredChannelsMutex.Lock()
if 0 < len(applied) && server.registeredChannels[channel.nameCasefolded] != nil && (banlistUpdated || exceptlistUpdated || invexlistUpdated) {
server.store.Update(func(tx *buntdb.Tx) error {
chanInfo := server.loadChannelNoMutex(tx, channel.nameCasefolded)
if banlistUpdated {
var banlist []string
for mask := range channel.lists[BanMask].masks {
banlist = append(banlist, mask)
}
chanInfo.Banlist = banlist
}
if exceptlistUpdated {
var exceptlist []string
for mask := range channel.lists[ExceptMask].masks {
exceptlist = append(exceptlist, mask)
}
chanInfo.Exceptlist = exceptlist
}
if invexlistUpdated {
var invitelist []string
for mask := range channel.lists[InviteMask].masks {
invitelist = append(invitelist, mask)
}
chanInfo.Invitelist = invitelist
}
server.saveChannelNoMutex(tx, channel.nameCasefolded, *chanInfo)
return nil
})
}
server.registeredChannelsMutex.Unlock()
// send out changes
2016-06-22 13:35:26 +02:00
if len(applied) > 0 {
//TODO(dan): we should change the name of String and make it return a slice here
args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
2017-10-23 01:50:16 +02:00
for _, member := range channel.Members() {
member.Send(nil, client.nickMaskString, "MODE", args...)
}
2016-06-22 13:35:26 +02:00
} else {
2017-10-23 01:50:16 +02:00
args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...)
2016-06-22 13:35:26 +02:00
client.Send(nil, client.nickMaskString, RPL_CHANNELMODEIS, args...)
client.Send(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10))
2016-06-22 13:35:26 +02:00
}
return false
}