mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-03 12:05:52 +01:00
merge with branch 1.x
This commit is contained in:
12
pom.xml
12
pom.xml
@@ -443,23 +443,25 @@
|
||||
<powermock.version>1.5.3</powermock.version>
|
||||
|
||||
<!-- logging libraries -->
|
||||
<slf4j.version>1.7.6</slf4j.version>
|
||||
<logback.version>1.1.1</logback.version>
|
||||
<slf4j.version>1.7.7</slf4j.version>
|
||||
<logback.version>1.1.2</logback.version>
|
||||
<servlet.version>2.5</servlet.version>
|
||||
<guice.version>3.0</guice.version>
|
||||
<jersey.version>1.18.1</jersey.version>
|
||||
<freemarker.version>2.3.20</freemarker.version>
|
||||
<jetty.version>7.6.14.v20131031</jetty.version>
|
||||
|
||||
<!-- event bus -->
|
||||
<legman.version>1.2.0</legman.version>
|
||||
|
||||
<!-- webserver -->
|
||||
<jetty.version>7.6.15.v20140411</jetty.version>
|
||||
|
||||
<!-- security libraries -->
|
||||
<shiro.version>1.2.3</shiro.version>
|
||||
|
||||
<!-- repostitory libraries -->
|
||||
<jgit.version>3.3.0.201403021825-r</jgit.version>
|
||||
<svnkit.version>1.8.4-scm2</svnkit.version>
|
||||
<jgit.version>3.3.2.201404171909-r</jgit.version>
|
||||
<svnkit.version>1.8.5-scm1</svnkit.version>
|
||||
|
||||
<!-- util libraries -->
|
||||
<guava.version>16.0.1</guava.version>
|
||||
|
||||
@@ -359,6 +359,19 @@ public class ScmConfiguration
|
||||
return forceBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the login attempt limit is enabled.
|
||||
*
|
||||
*
|
||||
* @return true if login attempt limit is enabled
|
||||
*
|
||||
* @since 1.37
|
||||
*/
|
||||
public boolean isLoginAttemptLimitEnabled()
|
||||
{
|
||||
return loginAttemptLimit > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if failed authenticators are skipped.
|
||||
*
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
|
||||
package sonia.scm.util;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.math.BigInteger;
|
||||
@@ -352,15 +356,14 @@ public final class Util
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
* Returns an emtpy string, if the object is null. Otherwise the result of
|
||||
* the toString method of the object is returned is returned.
|
||||
*
|
||||
* @param value object
|
||||
*
|
||||
* @since 1.13
|
||||
*
|
||||
* @return
|
||||
* @return string value or empty string
|
||||
*/
|
||||
public static String nonNull(Object value)
|
||||
{
|
||||
@@ -369,6 +372,23 @@ public final class Util
|
||||
: "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an emtpy string, if the string is null. Otherwise the string
|
||||
* is returned. The method is available to fix a possible linkage error which
|
||||
* was introduced with version 1.14. Please have a look at:
|
||||
* https://bitbucket.org/sdorra/scm-manager/issue/569/active-directory-plugin-not-working-in
|
||||
*
|
||||
* @param value string value
|
||||
*
|
||||
* @return string value or empty string
|
||||
*
|
||||
* @since 1.38
|
||||
*/
|
||||
public static String nonNull(String value)
|
||||
{
|
||||
return Strings.nullToEmpty(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -91,7 +91,6 @@ public class GitBasicAuthenticationFilter extends BasicAuthenticationFilter
|
||||
HttpServletResponse response)
|
||||
throws IOException
|
||||
{
|
||||
System.out.println(ClientMessages.get(request).failedAuthentication());
|
||||
if (GitUtil.isGitClient(request))
|
||||
{
|
||||
GitSmartHttpTools.sendError(request, response,
|
||||
|
||||
@@ -87,7 +87,8 @@ public class HgBasicAuthenticationFilter extends BasicAuthenticationFilter
|
||||
HttpServletResponse response)
|
||||
throws IOException
|
||||
{
|
||||
if (HgUtil.isHgClient(request))
|
||||
if (HgUtil.isHgClient(request)
|
||||
&& (configuration.isLoginAttemptLimitEnabled()))
|
||||
{
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@@ -30,18 +30,23 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.io.Closeables;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.SvnRepositoryHandler;
|
||||
import sonia.scm.repository.SvnUtil;
|
||||
import sonia.scm.repository.api.Command;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -72,6 +77,20 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
this.context = new SvnContext(handler.getDirectory(repository));
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
@Override
|
||||
public void close() throws IOException
|
||||
{
|
||||
Closeables.close(context, true);
|
||||
}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -149,8 +168,8 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private SvnContext context;
|
||||
private final SvnContext context;
|
||||
|
||||
/** Field description */
|
||||
private Repository repository;
|
||||
private final Repository repository;
|
||||
}
|
||||
|
||||
@@ -170,7 +170,9 @@ public abstract class RepositoryManagerTestBase
|
||||
Repository repository = createTestRepository();
|
||||
|
||||
repository.setArchived(true);
|
||||
delete(createRepositoryManager(true), repository);
|
||||
RepositoryManager drm = createRepositoryManager(true);
|
||||
drm.init(contextProvider);
|
||||
delete(drm, repository);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,6 +257,7 @@ public abstract class RepositoryManagerTestBase
|
||||
public void testListener() throws RepositoryException, IOException
|
||||
{
|
||||
RepositoryManager repoManager = createRepositoryManager(false);
|
||||
repoManager.init(contextProvider);
|
||||
TestListener listener = new TestListener();
|
||||
|
||||
ScmEventBus.getInstance().register(listener);
|
||||
|
||||
@@ -205,6 +205,14 @@
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- fix version conflict -->
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<version>4.2.6</version>
|
||||
</dependency>
|
||||
|
||||
<!-- template engine -->
|
||||
|
||||
<dependency>
|
||||
@@ -477,7 +485,7 @@
|
||||
</systemProperty>
|
||||
<systemProperty>
|
||||
<name>scm.stage</name>
|
||||
<value>${scm.stage}</value>
|
||||
<value>production</value>
|
||||
</systemProperty>
|
||||
<systemProperty>
|
||||
<name>java.awt.headless</name>
|
||||
|
||||
@@ -196,9 +196,10 @@ public class ScmContextListener extends GuiceServletContextListener
|
||||
moduleList.addAll(pluginLoader.getInjectionModules());
|
||||
moduleList.addAll(overrides.getModules());
|
||||
|
||||
SCMContextProvider ctx = SCMContext.getContext();
|
||||
|
||||
return Guice.createInjector(ctx.getStage().getInjectionStage(), moduleList);
|
||||
// TODO: fix cyclic dependencies in production environment
|
||||
// SCMContextProvider ctx = SCMContext.getContext();
|
||||
// return Guice.createInjector(ctx.getStage().getInjectionStage(), moduleList);
|
||||
return Guice.createInjector(moduleList);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -202,7 +202,7 @@ public class ScmServletModule extends JerseyServletModule
|
||||
PATTERN_STYLESHEET, "*.json", "*.xml", "*.txt" };
|
||||
|
||||
/** Field description */
|
||||
private static Logger logger =
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(ScmServletModule.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer. 2. Redistributions in
|
||||
* binary form must reproduce the above copyright notice, this list of
|
||||
* conditions and the following disclaimer in the documentation and/or other
|
||||
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
|
||||
* nor the names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import sonia.scm.EagerSingleton;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.PrivilegedAction;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
* @since 1.37
|
||||
*/
|
||||
@Extension
|
||||
@EagerSingleton
|
||||
public final class LastModifiedUpdateListener
|
||||
{
|
||||
|
||||
/**
|
||||
* the logger for LastModifiedUpdateListener
|
||||
*/
|
||||
private static final Logger logger =
|
||||
LoggerFactory.getLogger(LastModifiedUpdateListener.class);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param adminContext
|
||||
* @param repositoryManager
|
||||
*/
|
||||
@Inject
|
||||
public LastModifiedUpdateListener(AdministrationContext adminContext,
|
||||
RepositoryManager repositoryManager)
|
||||
{
|
||||
this.adminContext = adminContext;
|
||||
this.repositoryManager = repositoryManager;
|
||||
}
|
||||
|
||||
//~--- methods --------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
@Subscribe
|
||||
public void onPostReceive(PostReceiveRepositoryHookEvent event)
|
||||
{
|
||||
final Repository repository = event.getRepository();
|
||||
|
||||
if (repository != null)
|
||||
{
|
||||
//J-
|
||||
adminContext.runAsAdmin(
|
||||
new LastModifiedPrivilegedAction(repositoryManager, repository)
|
||||
);
|
||||
//J+
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("recevied hook without repository");
|
||||
}
|
||||
}
|
||||
|
||||
//~--- inner classes --------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Class description
|
||||
*
|
||||
*
|
||||
* @version Enter version here..., 14/04/20
|
||||
* @author Enter your name here...
|
||||
*/
|
||||
static class LastModifiedPrivilegedAction implements PrivilegedAction
|
||||
{
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*
|
||||
* @param repositoryManager
|
||||
* @param repository
|
||||
*/
|
||||
public LastModifiedPrivilegedAction(RepositoryManager repositoryManager,
|
||||
Repository repository)
|
||||
{
|
||||
this.repositoryManager = repositoryManager;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
//~--- methods ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
Repository dbr = repositoryManager.get(repository.getId());
|
||||
|
||||
if (dbr != null)
|
||||
{
|
||||
logger.info("update last modified date of repository {}", dbr.getId());
|
||||
dbr.setLastModified(System.currentTimeMillis());
|
||||
|
||||
try
|
||||
{
|
||||
repositoryManager.modify(dbr);
|
||||
}
|
||||
catch (RepositoryException ex)
|
||||
{
|
||||
logger.error("could not modify repository", ex);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger.error("could not modify repository", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.error("could not find repository with id {}",
|
||||
repository.getId());
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields -------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final Repository repository;
|
||||
|
||||
/** Field description */
|
||||
private final RepositoryManager repositoryManager;
|
||||
}
|
||||
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private final AdministrationContext adminContext;
|
||||
|
||||
/** Field description */
|
||||
private final RepositoryManager repositoryManager;
|
||||
}
|
||||
@@ -86,8 +86,6 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
*
|
||||
*
|
||||
* @param injector
|
||||
* @param userSessionProvider
|
||||
* @param contextHolder
|
||||
* @param securityManager
|
||||
*/
|
||||
@Inject
|
||||
@@ -178,6 +176,22 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private Subject createAdminSubject()
|
||||
{
|
||||
//J-
|
||||
return new Subject.Builder(securityManager)
|
||||
.authenticated(true)
|
||||
.principals(principalCollection)
|
||||
.buildSubject();
|
||||
//J+
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
@@ -195,12 +209,7 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
{
|
||||
SecurityUtils.setSecurityManager(securityManager);
|
||||
|
||||
//J-
|
||||
Subject subject = new Subject.Builder(securityManager)
|
||||
.authenticated(true)
|
||||
.principals(principalCollection)
|
||||
.buildSubject();
|
||||
//J+
|
||||
Subject subject = createAdminSubject();
|
||||
ThreadState state = new SubjectThreadState(subject);
|
||||
|
||||
state.bind();
|
||||
@@ -240,7 +249,7 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
|
||||
if (logger.isInfoEnabled())
|
||||
{
|
||||
String username = null;
|
||||
String username;
|
||||
|
||||
if (subject.hasRole(Role.USER))
|
||||
{
|
||||
@@ -255,7 +264,12 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
action.getClass().getName());
|
||||
}
|
||||
|
||||
subject.runAs(principalCollection);
|
||||
Subject adminSubject = createAdminSubject();
|
||||
|
||||
// do not use runas, because we want only execute this action in this
|
||||
// thread as administrator. Runas could affect other threads
|
||||
|
||||
ThreadContext.bind(adminSubject);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -263,32 +277,20 @@ public class DefaultAdministrationContext implements AdministrationContext
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
PrincipalCollection collection = subject.releaseRunAs();
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("release runas for user {}/{}",
|
||||
principal, collection.getPrimaryPrincipal());
|
||||
}
|
||||
|
||||
if (!subject.getPrincipal().equals(principal))
|
||||
{
|
||||
logger.error("release runas failed, {} is not equal with {}, logout.",
|
||||
subject.getPrincipal(), principal);
|
||||
subject.logout();
|
||||
}
|
||||
logger.debug("release administration context for user {}/{}", principal,
|
||||
subject.getPrincipal());
|
||||
ThreadContext.bind(subject);
|
||||
}
|
||||
}
|
||||
|
||||
//~--- fields ---------------------------------------------------------------
|
||||
|
||||
/** Field description */
|
||||
private Injector injector;
|
||||
private final Injector injector;
|
||||
|
||||
/** Field description */
|
||||
private final org.apache.shiro.mgt.SecurityManager securityManager;
|
||||
|
||||
/** Field description */
|
||||
private PrincipalCollection principalCollection;
|
||||
|
||||
/** Field description */
|
||||
private org.apache.shiro.mgt.SecurityManager securityManager;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<!-- encoders are by default assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
@@ -61,14 +61,14 @@
|
||||
|
||||
<append>true</append>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
|
||||
</encoder>
|
||||
|
||||
</appender>
|
||||
|
||||
@@ -163,6 +163,7 @@ if ( Sonia.repository.Grid ){
|
||||
colContactText: 'Kontakt',
|
||||
colDescriptionText: 'Beschreibung',
|
||||
colCreationDateText: 'Erstellungsdatum',
|
||||
colLastModifiedText: 'Zuletzt geändert',
|
||||
colUrlText: 'Url',
|
||||
colArchiveText: 'Archiv',
|
||||
emptyText: 'Es wurde kein Repository konfiguriert',
|
||||
|
||||
@@ -38,6 +38,7 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, {
|
||||
colContactText: 'Contact',
|
||||
colDescriptionText: 'Description',
|
||||
colCreationDateText: 'Creation date',
|
||||
colLastModifiedText: 'Last modified',
|
||||
colUrlText: 'Url',
|
||||
colArchiveText: 'Archive',
|
||||
emptyText: 'No repository is configured',
|
||||
@@ -90,6 +91,8 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, {
|
||||
name: 'description'
|
||||
},{
|
||||
name: 'creationDate'
|
||||
},{
|
||||
name: 'lastModified'
|
||||
},{
|
||||
name: 'public'
|
||||
},{
|
||||
@@ -159,6 +162,12 @@ Sonia.repository.Grid = Ext.extend(Sonia.rest.Grid, {
|
||||
header: this.colCreationDateText,
|
||||
dataIndex: 'creationDate',
|
||||
renderer: Ext.util.Format.formatTimestamp
|
||||
},{
|
||||
id: 'lastModified',
|
||||
header: this.colLastModifiedText,
|
||||
dataIndex: 'lastModified',
|
||||
renderer: Ext.util.Format.formatTimestamp,
|
||||
hidden: true
|
||||
},{
|
||||
id: 'Url',
|
||||
header: this.colUrlText,
|
||||
|
||||
@@ -575,8 +575,19 @@ Sonia.scm.Main = Ext.extend(Ext.util.Observable, {
|
||||
|
||||
Ext.onReady(function(){
|
||||
|
||||
function isLocalStorageAvailable(){
|
||||
var mod = '__scm-manager';
|
||||
try {
|
||||
localStorage.setItem(mod, mod);
|
||||
localStorage.removeItem(mod);
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var stateProvider;
|
||||
if ( typeof(Storage) !== "undefined" ){
|
||||
if (isLocalStorageAvailable()){
|
||||
if (debug){
|
||||
console.debug('use localStore to save application state');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user