4 changed files with 289 additions and 287 deletions
@ -1,83 +1,86 @@ |
|||||||
package ru.simsonic.rscPermissions.API; |
package ru.simsonic.rscPermissions.API; |
||||||
|
|
||||||
import ru.simsonic.rscPermissions.Engine.Matchers; |
import ru.simsonic.rscMinecraftLibrary.Bukkit.GenericChatCodes; |
||||||
|
import ru.simsonic.rscPermissions.Engine.Matchers; |
||||||
public enum PlayerType |
|
||||||
{ |
public enum PlayerType |
||||||
NAME(0), // 16 chars [_a-zA-Z0-9] max
|
{ |
||||||
UUID(1), // 550e8400-e29b-41d4-a716-446655440000
|
NAME(0), // 16 chars [_a-zA-Z0-9] max
|
||||||
DASHLESS_UUID(2), // 550e8400e29b41d4a716446655440000
|
UUID(1), // 550e8400-e29b-41d4-a716-446655440000
|
||||||
INTERNET_WILDCARD(3), // 192.168.*.*
|
DASHLESS_UUID(2), // 550e8400e29b41d4a716446655440000
|
||||||
INTERNET_SUBNETMASK(4), // 192.168.0.0/16
|
INTERNET_WILDCARD(3), // 192.168.*.*
|
||||||
INAPPLICABLE(-1); |
INTERNET_SUBNETMASK(4), // 192.168.0.0/16
|
||||||
private final int value; |
INAPPLICABLE(-1); |
||||||
private PlayerType(int value) |
private final int value; |
||||||
{ |
private PlayerType(int value) |
||||||
this.value = value; |
{ |
||||||
} |
this.value = value; |
||||||
public static PlayerType byValue(int value) |
} |
||||||
{ |
public static PlayerType byValue(int value) |
||||||
for(PlayerType constant : PlayerType.values()) |
{ |
||||||
if(constant.value == value) |
for(PlayerType constant : PlayerType.values()) |
||||||
return constant; |
if(constant.value == value) |
||||||
return INAPPLICABLE; |
return constant; |
||||||
} |
return INAPPLICABLE; |
||||||
public static PlayerType scanPlayerEntity(String entity) |
} |
||||||
{ |
public static PlayerType scanPlayerEntity(String entity) |
||||||
if(entity == null || "".equals(entity)) |
{ |
||||||
return NAME; |
if(entity == null || "".equals(entity)) |
||||||
if(Matchers.isCorrectNickname(entity)) |
return NAME; |
||||||
return NAME; |
if(Matchers.isCorrectNickname(entity)) |
||||||
if(Matchers.isCorrectUUID(entity)) |
return NAME; |
||||||
return UUID; |
if(Matchers.isCorrectUUID(entity)) |
||||||
if(Matchers.isCorrectDashlessUUID(entity)) |
return UUID; |
||||||
return DASHLESS_UUID; |
if(Matchers.isCorrectDashlessUUID(entity)) |
||||||
if(Matchers.isCorrectWildcard(entity)) |
return DASHLESS_UUID; |
||||||
return INTERNET_WILDCARD; |
if(Matchers.isCorrectWildcard(entity)) |
||||||
if(Matchers.isCorrectSubnetMask(entity)) |
return INTERNET_WILDCARD; |
||||||
return INTERNET_SUBNETMASK; |
if(Matchers.isCorrectSubnetMask(entity)) |
||||||
return INAPPLICABLE; |
return INTERNET_SUBNETMASK; |
||||||
} |
return INAPPLICABLE; |
||||||
public static String normalize(String entity) |
} |
||||||
{ |
public static String normalize(String entity) |
||||||
if(entity == null || "".equals(entity)) |
{ |
||||||
return ""; |
if(entity == null || "".equals(entity)) |
||||||
if(Matchers.isCorrectDashlessUUID(entity)) |
return ""; |
||||||
return Matchers.uuidAddDashes(entity); |
if(Matchers.isCorrectDashlessUUID(entity)) |
||||||
return entity; |
return Matchers.uuidAddDashes(entity); |
||||||
} |
return entity; |
||||||
public boolean isEntityApplicable(String entity, String identifier) |
} |
||||||
{ |
public boolean isEntityApplicable(String entity, String identifier) |
||||||
if(entity == null || "".equals(entity) || identifier == null || "".equals(identifier)) |
{ |
||||||
return false; |
if(entity == null || "".equals(entity) || identifier == null || "".equals(identifier)) |
||||||
try |
return false; |
||||||
{ |
try |
||||||
switch(this) |
{ |
||||||
{ |
switch(this) |
||||||
case NAME: |
{ |
||||||
return identifier.equals(entity); |
case NAME: |
||||||
case DASHLESS_UUID: |
return entity.contains("*") |
||||||
if(Matchers.isCorrectUUID(identifier)) |
? GenericChatCodes.wildcardMatch(identifier, entity) |
||||||
identifier = Matchers.uuidRemoveDashes(identifier); |
: identifier.equals(entity); |
||||||
if(Matchers.isCorrectDashlessUUID(identifier)) |
case DASHLESS_UUID: |
||||||
return entity.equalsIgnoreCase(identifier); |
if(Matchers.isCorrectUUID(identifier)) |
||||||
break; |
identifier = Matchers.uuidRemoveDashes(identifier); |
||||||
case UUID: |
if(Matchers.isCorrectDashlessUUID(identifier)) |
||||||
if(Matchers.isCorrectDashlessUUID(identifier)) |
return entity.equalsIgnoreCase(identifier); |
||||||
identifier = Matchers.uuidAddDashes(identifier); |
break; |
||||||
if(Matchers.isCorrectUUID(identifier)) |
case UUID: |
||||||
return entity.equalsIgnoreCase(identifier); |
if(Matchers.isCorrectDashlessUUID(identifier)) |
||||||
break; |
identifier = Matchers.uuidAddDashes(identifier); |
||||||
case INTERNET_WILDCARD: |
if(Matchers.isCorrectUUID(identifier)) |
||||||
case INTERNET_SUBNETMASK: |
return entity.equalsIgnoreCase(identifier); |
||||||
// TO DO HERE
|
break; |
||||||
return false; |
case INTERNET_WILDCARD: |
||||||
case INAPPLICABLE: |
case INTERNET_SUBNETMASK: |
||||||
default: |
// TO DO HERE
|
||||||
break; |
return false; |
||||||
} |
case INAPPLICABLE: |
||||||
} catch(IllegalArgumentException ex) { |
default: |
||||||
} |
break; |
||||||
return false; |
} |
||||||
} |
} catch(IllegalArgumentException ex) { |
||||||
} |
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
@ -1,153 +1,152 @@ |
|||||||
package ru.simsonic.rscPermissions; |
package ru.simsonic.rscPermissions; |
||||||
|
|
||||||
import java.io.IOException; |
import java.io.IOException; |
||||||
import java.util.logging.Level; |
import java.util.logging.Level; |
||||||
import java.util.logging.Logger; |
import java.util.logging.Logger; |
||||||
import org.bukkit.Bukkit; |
import org.bukkit.Bukkit; |
||||||
import org.bukkit.command.Command; |
import org.bukkit.command.Command; |
||||||
import org.bukkit.command.CommandSender; |
import org.bukkit.command.CommandSender; |
||||||
import org.bukkit.command.ConsoleCommandSender; |
import org.bukkit.plugin.java.JavaPlugin; |
||||||
import org.bukkit.plugin.java.JavaPlugin; |
import org.bukkit.scheduler.BukkitScheduler; |
||||||
import org.bukkit.scheduler.BukkitScheduler; |
import org.mcstats.MetricsLite; |
||||||
import org.mcstats.MetricsLite; |
import ru.simsonic.rscMinecraftLibrary.Bukkit.CommandAnswerException; |
||||||
import ru.simsonic.rscMinecraftLibrary.Bukkit.CommandAnswerException; |
import ru.simsonic.rscMinecraftLibrary.Bukkit.GenericChatCodes; |
||||||
import ru.simsonic.rscMinecraftLibrary.Bukkit.GenericChatCodes; |
import ru.simsonic.rscPermissions.API.Settings; |
||||||
import ru.simsonic.rscPermissions.API.Settings; |
import ru.simsonic.rscPermissions.Bukkit.BukkitEventListener; |
||||||
import ru.simsonic.rscPermissions.Bukkit.BukkitEventListener; |
import ru.simsonic.rscPermissions.Bukkit.BukkitPermissionManager; |
||||||
import ru.simsonic.rscPermissions.Bukkit.BukkitPermissionManager; |
import ru.simsonic.rscPermissions.Bukkit.BukkitPluginConfiguration; |
||||||
import ru.simsonic.rscPermissions.Bukkit.BukkitPluginConfiguration; |
import ru.simsonic.rscPermissions.Bukkit.BukkitRegionProviders; |
||||||
import ru.simsonic.rscPermissions.Bukkit.BukkitRegionProviders; |
import ru.simsonic.rscPermissions.Bukkit.Commands.BukkitCommands; |
||||||
import ru.simsonic.rscPermissions.Bukkit.Commands.BukkitCommands; |
import ru.simsonic.rscPermissions.Bukkit.RegionUpdateObserver; |
||||||
import ru.simsonic.rscPermissions.Bukkit.RegionUpdateObserver; |
import ru.simsonic.rscPermissions.Engine.Backends.BackendDatabase; |
||||||
import ru.simsonic.rscPermissions.Engine.Backends.BackendDatabase; |
import ru.simsonic.rscPermissions.Engine.Backends.BackendJson; |
||||||
import ru.simsonic.rscPermissions.Engine.Backends.BackendJson; |
import ru.simsonic.rscPermissions.Engine.Backends.DatabaseContents; |
||||||
import ru.simsonic.rscPermissions.Engine.Backends.DatabaseContents; |
import ru.simsonic.rscPermissions.Engine.InternalCache; |
||||||
import ru.simsonic.rscPermissions.Engine.InternalCache; |
import ru.simsonic.rscPermissions.Engine.Phrases; |
||||||
import ru.simsonic.rscPermissions.Engine.Phrases; |
|
||||||
|
public final class BukkitPluginMain extends JavaPlugin |
||||||
public final class BukkitPluginMain extends JavaPlugin |
{ |
||||||
{ |
public static final Logger consoleLog = Bukkit.getLogger(); |
||||||
public static final Logger consoleLog = Bukkit.getLogger(); |
public final Settings settings = new BukkitPluginConfiguration(this); |
||||||
public final Settings settings = new BukkitPluginConfiguration(this); |
public final BridgeForBukkitAPI bridgeForBukkit = new BridgeForBukkitAPI(this); |
||||||
public final BridgeForBukkitAPI bridgeForBukkit = new BridgeForBukkitAPI(this); |
public final BukkitEventListener bukkitListener = new BukkitEventListener(this); |
||||||
public final BukkitEventListener bukkitListener = new BukkitEventListener(this); |
public final BackendJson localStorage = new BackendJson(getDataFolder()); |
||||||
public final BackendJson localStorage = new BackendJson(getDataFolder()); |
public final BackendDatabase connection = new BackendDatabase(consoleLog); |
||||||
public final BackendDatabase connection = new BackendDatabase(consoleLog); |
public final InternalCache internalCache = new InternalCache(); |
||||||
public final InternalCache internalCache = new InternalCache(); |
public final BukkitPermissionManager permissionManager = new BukkitPermissionManager(this); |
||||||
public final BukkitPermissionManager permissionManager = new BukkitPermissionManager(this); |
public final BukkitRegionProviders regionListProvider = new BukkitRegionProviders(this); |
||||||
public final BukkitRegionProviders regionListProvider = new BukkitRegionProviders(this); |
private final RegionUpdateObserver regionUpdateObserver = new RegionUpdateObserver(this); |
||||||
private final RegionUpdateObserver regionUpdateObserver = new RegionUpdateObserver(this); |
public final BukkitCommands commandHelper = new BukkitCommands(this); |
||||||
public final BukkitCommands commandHelper = new BukkitCommands(this); |
private MetricsLite metrics; |
||||||
private MetricsLite metrics; |
@Override |
||||||
@Override |
public void onLoad() |
||||||
public void onLoad() |
{ |
||||||
{ |
Phrases.extractTranslations(getDataFolder()); |
||||||
Phrases.extractTranslations(getDataFolder()); |
settings.onLoad(); |
||||||
settings.onLoad(); |
consoleLog.log(Level.INFO, "[rscp] serverId value is set to \"{0}\". You can change it in server.properties.", getServer().getServerId()); |
||||||
consoleLog.log(Level.INFO, "[rscp] serverId value is set to \"{0}\". You can change it in server.properties.", getServer().getServerId()); |
consoleLog.log(Level.INFO, "[rscp] rscPermissions has been loaded."); |
||||||
consoleLog.log(Level.INFO, "[rscp] rscPermissions has been loaded."); |
} |
||||||
} |
@Override |
||||||
@Override |
public void onEnable() |
||||||
public void onEnable() |
{ |
||||||
{ |
// Read settings and setup components
|
||||||
// Read settings and setup components
|
settings.readSettings(); |
||||||
settings.readSettings(); |
bukkitListener.onEnable(); |
||||||
bukkitListener.onEnable(); |
internalCache.setDefaultGroup( |
||||||
internalCache.setDefaultGroup( |
settings.getDefaultGroup(), |
||||||
settings.getDefaultGroup(), |
settings.isDefaultForever(), |
||||||
settings.isDefaultForever(), |
settings.isUsingAncestorPrefixes()); |
||||||
settings.isUsingAncestorPrefixes()); |
Phrases.applyTranslation(settings.getTranslationProvider()); |
||||||
Phrases.applyTranslation(settings.getTranslationProvider()); |
// Restore temporary cached data from json files
|
||||||
// Restore temporary cached data from json files
|
final DatabaseContents contents = localStorage.retrieveContents(); |
||||||
final DatabaseContents contents = localStorage.retrieveContents(); |
contents.filterServerId(getServer().getServerId()).filterLifetime(); |
||||||
contents.filterServerId(getServer().getServerId()).filterLifetime(); |
internalCache.fill(contents); |
||||||
internalCache.fill(contents); |
getServer().getConsoleSender().sendMessage(GenericChatCodes.processStringStatic( |
||||||
getServer().getConsoleSender().sendMessage(GenericChatCodes.processStringStatic( |
(Settings.chatPrefix + Phrases.FETCHED_LOCAL_CACHE.toString()) |
||||||
(Settings.chatPrefix + Phrases.FETCHED_LOCAL_CACHE.toString()) |
.replace("{:E}", String.valueOf(contents.entities.length)) |
||||||
.replace("{:E}", String.valueOf(contents.entities.length)) |
.replace("{:P}", String.valueOf(contents.permissions.length)) |
||||||
.replace("{:P}", String.valueOf(contents.permissions.length)) |
.replace("{:I}", String.valueOf(contents.inheritance.length)))); |
||||||
.replace("{:I}", String.valueOf(contents.inheritance.length)))); |
// Integrate Metrics
|
||||||
// Integrate Metrics
|
if(settings.isUseMetrics()) |
||||||
if(settings.isUseMetrics()) |
try |
||||||
try |
{ |
||||||
{ |
metrics = new MetricsLite(this); |
||||||
metrics = new MetricsLite(this); |
metrics.start(); |
||||||
metrics.start(); |
consoleLog.info(Phrases.PLUGIN_METRICS.toString()); |
||||||
consoleLog.info(Phrases.PLUGIN_METRICS.toString()); |
} catch(IOException ex) { |
||||||
} catch(IOException ex) { |
consoleLog.log(Level.WARNING, "[rscp][Metrics] Exception: {0}", ex); |
||||||
consoleLog.log(Level.WARNING, "[rscp][Metrics] Exception: {0}", ex); |
} |
||||||
} |
// Register event's dispatcher
|
||||||
// Register event's dispatcher
|
getServer().getPluginManager().registerEvents(bukkitListener, this); |
||||||
getServer().getPluginManager().registerEvents(bukkitListener, this); |
regionUpdateObserver.registerListeners(); |
||||||
regionUpdateObserver.registerListeners(); |
// Integrate Vault and WEPIF
|
||||||
// Integrate Vault and WEPIF
|
bridgeForBukkit.setupVault(); |
||||||
bridgeForBukkit.setupVault(); |
getServer().getScheduler().runTask(this, new Runnable() |
||||||
getServer().getScheduler().runTask(this, new Runnable() |
{ |
||||||
{ |
@Override |
||||||
@Override |
public void run() |
||||||
public void run() |
{ |
||||||
{ |
bridgeForBukkit.setupWEPIF(); |
||||||
bridgeForBukkit.setupWEPIF(); |
} |
||||||
} |
}); |
||||||
}); |
// WorldGuard, Residence and other possible region list providers
|
||||||
// WorldGuard, Residence and other possible region list providers
|
regionListProvider.integrate(); |
||||||
regionListProvider.integrate(); |
// Start all needed parallel threads as daemons
|
||||||
// Start all needed parallel threads as daemons
|
permissionManager.startDeamon(); |
||||||
permissionManager.startDeamon(); |
regionUpdateObserver.startDeamon(); |
||||||
regionUpdateObserver.startDeamon(); |
// Connect to database and initiate data fetching
|
||||||
// Connect to database and initiate data fetching
|
connection.initialize(settings.getConnectionParams()); |
||||||
connection.initialize(settings.getConnectionParams()); |
if(settings.getAutoReloadDelayTicks() > 0) |
||||||
if(settings.getAutoReloadDelayTicks() > 0) |
commandHelper.threadFetchDatabaseContents.startDeamon(); |
||||||
commandHelper.threadFetchDatabaseContents.startDeamon(); |
// Done
|
||||||
// Done
|
consoleLog.info(Phrases.PLUGIN_ENABLED.toString()); |
||||||
consoleLog.info(Phrases.PLUGIN_ENABLED.toString()); |
} |
||||||
} |
@Override |
||||||
@Override |
public void onDisable() |
||||||
public void onDisable() |
{ |
||||||
{ |
getServer().getServicesManager().unregisterAll(this); |
||||||
getServer().getServicesManager().unregisterAll(this); |
regionUpdateObserver.stop(); |
||||||
regionUpdateObserver.stop(); |
permissionManager.stop(); |
||||||
permissionManager.stop(); |
internalCache.clear(); |
||||||
internalCache.clear(); |
connection.disconnect(); |
||||||
connection.disconnect(); |
regionListProvider.deintegrate(); |
||||||
regionListProvider.deintegrate(); |
metrics = null; |
||||||
metrics = null; |
consoleLog.info(Phrases.PLUGIN_DISABLED.toString()); |
||||||
consoleLog.info(Phrases.PLUGIN_DISABLED.toString()); |
} |
||||||
} |
private int nAutoUpdaterTaskId = -1; |
||||||
private int nAutoUpdaterTaskId = -1; |
public void scheduleAutoUpdate() |
||||||
public void scheduleAutoUpdate() |
{ |
||||||
{ |
final BukkitScheduler scheduler = getServer().getScheduler(); |
||||||
final BukkitScheduler scheduler = getServer().getScheduler(); |
if(nAutoUpdaterTaskId != -1) |
||||||
if(nAutoUpdaterTaskId != -1) |
scheduler.cancelTask(nAutoUpdaterTaskId); |
||||||
scheduler.cancelTask(nAutoUpdaterTaskId); |
final int delay = settings.getAutoReloadDelayTicks(); |
||||||
final int delay = settings.getAutoReloadDelayTicks(); |
nAutoUpdaterTaskId = delay > 0 |
||||||
nAutoUpdaterTaskId = delay > 0 |
? scheduler.scheduleSyncDelayedTask(this, new Runnable() |
||||||
? scheduler.scheduleSyncDelayedTask(this, new Runnable() |
{ |
||||||
{ |
@Override |
||||||
@Override |
public void run() |
||||||
public void run() |
{ |
||||||
{ |
commandHelper.threadFetchDatabaseContents.startDeamon(); |
||||||
commandHelper.threadFetchDatabaseContents.startDeamon(); |
} |
||||||
} |
}, delay) |
||||||
}, delay) |
: -1; |
||||||
: -1; |
} |
||||||
} |
@Override |
||||||
@Override |
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) |
||||||
public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) |
{ |
||||||
{ |
if(sender != null) |
||||||
if(sender != null) |
try |
||||||
try |
{ |
||||||
{ |
switch(cmd.getName().toLowerCase()) |
||||||
switch(cmd.getName().toLowerCase()) |
{ |
||||||
{ |
case "rscp": |
||||||
case "rscp": |
commandHelper.onCommandHub(sender, args); |
||||||
commandHelper.onCommandHub(sender, args); |
break; |
||||||
break; |
} |
||||||
} |
} catch(CommandAnswerException ex) { |
||||||
} catch(CommandAnswerException ex) { |
for(String answer : ex.getMessageArray()) |
||||||
for(String answer : ex.getMessageArray()) |
sender.sendMessage(GenericChatCodes.processStringStatic(Settings.chatPrefix + answer)); |
||||||
sender.sendMessage(GenericChatCodes.processStringStatic(Settings.chatPrefix + answer)); |
} |
||||||
} |
return true; |
||||||
return true; |
} |
||||||
} |
} |
||||||
} |
|
||||||
|
@ -1,50 +1,50 @@ |
|||||||
package ru.simsonic.rscPermissions.Engine; |
package ru.simsonic.rscPermissions.Engine; |
||||||
|
|
||||||
import java.util.ArrayList; |
import java.util.ArrayList; |
||||||
import java.util.Collections; |
import java.util.Collections; |
||||||
import java.util.List; |
import java.util.List; |
||||||
import java.util.Map; |
import java.util.Map; |
||||||
import ru.simsonic.rscMinecraftLibrary.Bukkit.GenericChatCodes; |
import ru.simsonic.rscMinecraftLibrary.Bukkit.GenericChatCodes; |
||||||
import ru.simsonic.rscPermissions.API.Settings; |
import ru.simsonic.rscPermissions.API.Settings; |
||||||
|
|
||||||
public class ResolutionResult |
public class ResolutionResult |
||||||
{ |
{ |
||||||
public String prefix = ""; |
public String prefix = ""; |
||||||
public String suffix = ""; |
public String suffix = ""; |
||||||
public Map<String, Boolean> permissions; |
public Map<String, Boolean> permissions; |
||||||
protected List<String> groups; |
protected List<String> groups; |
||||||
public boolean hasPermission(String permission) |
public boolean hasPermission(String permission) |
||||||
{ |
{ |
||||||
for(Map.Entry<String, Boolean> entry : permissions.entrySet()) |
for(Map.Entry<String, Boolean> entry : permissions.entrySet()) |
||||||
if(entry.getKey().equals(permission)) |
if(entry.getKey().equals(permission)) |
||||||
return entry.getValue(); |
return entry.getValue(); |
||||||
return false; |
return false; |
||||||
} |
} |
||||||
public boolean hasPermissionWC(String permission) |
public boolean hasPermissionWC(String permission) |
||||||
{ |
{ |
||||||
for(Map.Entry<String, Boolean> entry : permissions.entrySet()) |
for(Map.Entry<String, Boolean> entry : permissions.entrySet()) |
||||||
{ |
{ |
||||||
final String key = entry.getKey(); |
final String key = entry.getKey(); |
||||||
if(key.equals(permission)) |
if(key.equals(permission)) |
||||||
return entry.getValue(); |
return entry.getValue(); |
||||||
if(key.contains("*") && GenericChatCodes.wildcardMatch(permission, key)) |
if(key.contains("*") && GenericChatCodes.wildcardMatch(permission, key)) |
||||||
return entry.getValue(); |
return entry.getValue(); |
||||||
} |
} |
||||||
return false; |
return false; |
||||||
} |
} |
||||||
public List<String> getOrderedGroups() |
public List<String> getOrderedGroups() |
||||||
{ |
{ |
||||||
return Collections.unmodifiableList(groups); |
return Collections.unmodifiableList(groups); |
||||||
} |
} |
||||||
public String[] getDeorderedGroups() |
public String[] getDeorderedGroups() |
||||||
{ |
{ |
||||||
final ArrayList<String> list = new ArrayList(groups.size()); |
final ArrayList<String> list = new ArrayList(groups.size()); |
||||||
final String separator = new String(new char[] { Settings.groupLevelTab }); |
final String separator = new String(new char[] { Settings.groupLevelTab }); |
||||||
for(String group : groups) |
for(String group : groups) |
||||||
{ |
{ |
||||||
String[] splitted = group.split(separator); |
String[] splitted = group.split(separator); |
||||||
list.add(splitted[splitted.length - 1]); |
list.add(splitted[splitted.length - 1]); |
||||||
} |
} |
||||||
return list.toArray(new String[list.size()]); |
return list.toArray(new String[list.size()]); |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue