diff options
author | John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | 2021-03-27 10:37:22 +0100 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2022-10-19 10:02:51 +0200 |
commit | bd303388e3d4dae90c2266d183395db8321c11de (patch) | |
tree | fddc23eff000713596cd024914667320c8c1312e /core | |
parent | d5edcf8c9570618d9008b355b7f432575ff9d357 (diff) | |
download | nextcloud-server-bd303388e3d4dae90c2266d183395db8321c11de.tar.gz nextcloud-server-bd303388e3d4dae90c2266d183395db8321c11de.zip |
Cleanup ie and old edge properties
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'core')
-rw-r--r-- | core/Controller/UnsupportedBrowserController.php | 51 | ||||
-rw-r--r-- | core/css/apps.scss | 10 | ||||
-rw-r--r-- | core/css/public.scss | 6 | ||||
-rw-r--r-- | core/routes.php | 5 | ||||
-rw-r--r-- | core/src/Polyfill/closest.js | 41 | ||||
-rw-r--r-- | core/src/Polyfill/index.js | 3 | ||||
-rw-r--r-- | core/src/components/MainMenu.js | 1 | ||||
-rw-r--r-- | core/src/init.js | 12 | ||||
-rw-r--r-- | core/src/main.js | 17 | ||||
-rw-r--r-- | core/src/services/BrowserStorageService.js (renamed from core/src/Polyfill/console.js) | 24 | ||||
-rw-r--r-- | core/src/services/BrowsersListService.js | 30 | ||||
-rw-r--r-- | core/src/services/LoggerService.js (renamed from core/src/Polyfill/windows-phone.js) | 21 | ||||
-rw-r--r-- | core/src/unsupported-browser.js | 39 | ||||
-rw-r--r-- | core/src/utils/RedirectUnsupportedBrowsers.js | 54 | ||||
-rw-r--r-- | core/src/views/UnsupportedBrowser.vue | 191 | ||||
-rw-r--r-- | core/templates/unsupportedbrowser.php | 1 |
16 files changed, 406 insertions, 100 deletions
diff --git a/core/Controller/UnsupportedBrowserController.php b/core/Controller/UnsupportedBrowserController.php new file mode 100644 index 00000000000..8cdc190deea --- /dev/null +++ b/core/Controller/UnsupportedBrowserController.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IRequest; +use OCP\Util; + +class UnsupportedBrowserController extends Controller { + public function __construct(IRequest $request) { + parent::__construct('core', $request); + } + + /** + * @PublicPage + * @NoCSRFRequired + * + * @return Response + */ + public function index(): Response { + Util::addScript('core', 'unsupported-browser'); + Util::addStyle('core', 'icons'); + return new TemplateResponse('core', 'unsupportedbrowser', [], TemplateResponse::RENDER_AS_ERROR); + } +} diff --git a/core/css/apps.scss b/core/css/apps.scss index dea8cbf9217..f0d96d55e9e 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -970,16 +970,6 @@ $popoveritem-height: 44px; $popovericon-size: 16px; $outter-margin: math.div($popoveritem-height - $popovericon-size, 2); -.ie, -.edge { - .bubble, .bubble:after, - .popovermenu, .popovermenu:after, - #app-navigation .app-navigation-entry-menu, - #app-navigation .app-navigation-entry-menu:after { - border: 1px solid var(--color-border); - } -} - .contact .popovermenu ul, .popover__menu { > li > a > img { diff --git a/core/css/public.scss b/core/css/public.scss index 0e75a938cfa..ddeb444970d 100644 --- a/core/css/public.scss +++ b/core/css/public.scss @@ -47,12 +47,6 @@ $footer-height: 65px; padding-top: 0; } - /* force layout to make sure the content element's height matches its contents' height */ - .ie #content { - display: inline-block; - } - - p.info { margin: 20px auto; text-shadow: 0 0 2px rgba(0, 0, 0, .4); diff --git a/core/routes.php b/core/routes.php index 820db44bf90..02e27c9cfaf 100644 --- a/core/routes.php +++ b/core/routes.php @@ -58,11 +58,13 @@ $application->registerRoutes($this, [ ['name' => 'login#confirmPassword', 'url' => '/login/confirm', 'verb' => 'POST'], ['name' => 'login#showLoginForm', 'url' => '/login', 'verb' => 'GET'], ['name' => 'login#logout', 'url' => '/logout', 'verb' => 'GET'], + // Original login flow used by all clients ['name' => 'ClientFlowLogin#showAuthPickerPage', 'url' => '/login/flow', 'verb' => 'GET'], ['name' => 'ClientFlowLogin#generateAppPassword', 'url' => '/login/flow', 'verb' => 'POST'], ['name' => 'ClientFlowLogin#grantPage', 'url' => '/login/flow/grant', 'verb' => 'GET'], ['name' => 'ClientFlowLogin#apptokenRedirect', 'url' => '/login/flow/apptoken', 'verb' => 'POST'], + // NG login flow used by desktop client in case of Kerberos/fancy 2fa (smart cards for example) ['name' => 'ClientFlowLoginV2#poll', 'url' => '/login/v2/poll', 'verb' => 'POST'], ['name' => 'ClientFlowLoginV2#showAuthPickerPage', 'url' => '/login/v2/flow', 'verb' => 'GET'], @@ -97,6 +99,9 @@ $application->registerRoutes($this, [ // Well known requests https://tools.ietf.org/html/rfc5785 ['name' => 'WellKnown#handle', 'url' => '.well-known/{service}'], + + // Unsupported browser + ['name' => 'UnsupportedBrowser#index', 'url' => 'unsupported'], ], 'ocs' => [ ['root' => '/cloud', 'name' => 'OCS#getCapabilities', 'url' => '/capabilities', 'verb' => 'GET'], diff --git a/core/src/Polyfill/closest.js b/core/src/Polyfill/closest.js deleted file mode 100644 index 68751fad38c..00000000000 --- a/core/src/Polyfill/closest.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @copyright Copyright (c) 2016 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill - -if (!Element.prototype.matches) { - Element.prototype.matches - = Element.prototype.msMatchesSelector - || Element.prototype.webkitMatchesSelector -} - -if (!Element.prototype.closest) { - Element.prototype.closest = function(s) { - let el = this - - do { - if (el.matches(s)) return el - el = el.parentElement || el.parentNode - } while (el !== null && el.nodeType === 1) - return null - } -} diff --git a/core/src/Polyfill/index.js b/core/src/Polyfill/index.js index 5a190318327..610619217d2 100644 --- a/core/src/Polyfill/index.js +++ b/core/src/Polyfill/index.js @@ -21,7 +21,4 @@ * */ -import './console' -import './closest' -import './windows-phone' import 'focus-visible' diff --git a/core/src/components/MainMenu.js b/core/src/components/MainMenu.js index 267a3d9a361..be27e752237 100644 --- a/core/src/components/MainMenu.js +++ b/core/src/components/MainMenu.js @@ -28,7 +28,6 @@ import Vue from 'vue' import AppMenu from './AppMenu.vue' export const setUp = () => { - Vue.mixin({ methods: { t, diff --git a/core/src/init.js b/core/src/init.js index ae8db0abf49..867ba94483f 100644 --- a/core/src/init.js +++ b/core/src/init.js @@ -29,12 +29,12 @@ import _ from 'underscore' import $ from 'jquery' import moment from 'moment' -import { initSessionHeartBeat } from './session-heartbeat' -import OC from './OC/index' -import { setUp as setUpContactsMenu } from './components/ContactsMenu' -import { setUp as setUpMainMenu } from './components/MainMenu' -import { setUp as setUpUserMenu } from './components/UserMenu' -import PasswordConfirmation from './OC/password-confirmation' +import { initSessionHeartBeat } from './session-heartbeat.js' +import OC from './OC/index.js' +import { setUp as setUpContactsMenu } from './components/ContactsMenu.js' +import { setUp as setUpMainMenu } from './components/MainMenu.js' +import { setUp as setUpUserMenu } from './components/UserMenu.js' +import PasswordConfirmation from './OC/password-confirmation.js' // keep in sync with core/css/variables.scss const breakpointMobileWidth = 1024 diff --git a/core/src/main.js b/core/src/main.js index f9d89327a6a..959110e86a4 100644 --- a/core/src/main.js +++ b/core/src/main.js @@ -26,16 +26,21 @@ import $ from 'jquery' import 'core-js/stable' import 'regenerator-runtime/runtime' -import './Polyfill/index' +import './Polyfill/index.js' // If you remove the line below, tests won't pass // eslint-disable-next-line no-unused-vars -import OC from './OC/index' +import OC from './OC/index.js' -import './globals' -import './jquery/index' -import { initCore } from './init' -import { registerAppsSlideToggle } from './OC/apps' +import './globals.js' +import './jquery/index.js' +import { initCore } from './init.js' +import { registerAppsSlideToggle } from './OC/apps.js' +import { testSupportedBrowser } from './utils/RedirectUnsupportedBrowsers.js' + +if (window.TESTING === undefined) { + testSupportedBrowser() +} window.addEventListener('DOMContentLoaded', function() { initCore() diff --git a/core/src/Polyfill/console.js b/core/src/services/BrowserStorageService.js index be3aca1a9b3..d383e1caaaf 100644 --- a/core/src/Polyfill/console.js +++ b/core/src/services/BrowserStorageService.js @@ -1,10 +1,9 @@ /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author John Molakvoæ <skjnldsv@protonmail.com> * - * @license AGPL-3.0-or-later + * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -17,18 +16,13 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -/* eslint-disable no-console */ -if (typeof console === 'undefined' || typeof console.log === 'undefined') { - if (!window.console) { - window.console = {} - } - const noOp = () => {} - const methods = ['log', 'debug', 'warn', 'info', 'error', 'assert', 'time', 'timeEnd'] - for (let i = 0; i < methods.length; i++) { - console[methods[i]] = noOp - } -} +import { getBuilder } from '@nextcloud/browser-storage' + +export default getBuilder('nextcloud') + .clearOnLogout() + .persist() + .build() diff --git a/core/src/services/BrowsersListService.js b/core/src/services/BrowsersListService.js new file mode 100644 index 00000000000..c5d546665dc --- /dev/null +++ b/core/src/services/BrowsersListService.js @@ -0,0 +1,30 @@ +/** + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import { getUserAgentRegExp } from 'browserslist-useragent-regexp' +// eslint-disable-next-line node/no-extraneous-import +import browserslist from 'browserslist' +import browserslistConfig from '@nextcloud/browserslist-config' + +// Generate a regex that matches user agents to detect incompatible browsers +export const supportedBrowsersRegExp = getUserAgentRegExp({ allowHigherVersions: true, browsers: browserslistConfig }) +export const supportedBrowsers = browserslist(browserslistConfig) diff --git a/core/src/Polyfill/windows-phone.js b/core/src/services/LoggerService.js index 27b45d701c6..f0b8fc9e61d 100644 --- a/core/src/Polyfill/windows-phone.js +++ b/core/src/services/LoggerService.js @@ -1,10 +1,9 @@ /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> * @author John Molakvoæ <skjnldsv@protonmail.com> * - * @license AGPL-3.0-or-later + * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -17,15 +16,13 @@ * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. + * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -// fix device width on windows phone -if ('-ms-user-select' in document.documentElement.style && navigator.userAgent.match(/IEMobile\/10\.0/)) { - const msViewportStyle = document.createElement('style') - msViewportStyle.appendChild( - document.createTextNode('@-ms-viewport{width:auto!important}') - ) - document.getElementsByTagName('head')[0].appendChild(msViewportStyle) -} +import { getLoggerBuilder } from '@nextcloud/logger' + +export default getLoggerBuilder() + .setApp('core') + .detectUser() + .build() diff --git a/core/src/unsupported-browser.js b/core/src/unsupported-browser.js new file mode 100644 index 00000000000..cac5f145a7b --- /dev/null +++ b/core/src/unsupported-browser.js @@ -0,0 +1,39 @@ +/** + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import { generateUrl } from '@nextcloud/router' +import Vue from 'vue' + +import { browserStorageKey } from './utils/RedirectUnsupportedBrowsers.js' +import browserStorage from './services/BrowserStorageService.js' +import UnsupportedBrowser from './views/UnsupportedBrowser.vue' + +// If the ignore token is set, redirect +if (browserStorage.getItem(browserStorageKey) === 'true') { + window.location = generateUrl('/') +} + +export default new Vue({ + el: '#unsupported-browser', + // eslint-disable-next-line vue/match-component-file-name + name: 'UnsupportedBrowserRoot', + render: h => h(UnsupportedBrowser), +}) diff --git a/core/src/utils/RedirectUnsupportedBrowsers.js b/core/src/utils/RedirectUnsupportedBrowsers.js new file mode 100644 index 00000000000..74074cec558 --- /dev/null +++ b/core/src/utils/RedirectUnsupportedBrowsers.js @@ -0,0 +1,54 @@ +/** + * @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +import { generateUrl } from '@nextcloud/router' + +import { supportedBrowsersRegExp } from '../services/BrowsersListService.js' +import browserStorage from '../services/BrowserStorageService.js' +import logger from '../services/LoggerService.js' + +const redirectPath = '/unsupported' +export const browserStorageKey = 'unsupported-browser-ignore' + +const isBrowserOverridden = browserStorage.getItem(browserStorageKey) === 'true' + +/** + * Test the current browser user agent against our official browserslist config + * and redirect if unsupported + */ +export const testSupportedBrowser = function() { + if (supportedBrowsersRegExp.test(navigator.userAgent)) { + logger.debug('this browser is officially supported ! 🚀') + return + } + + // If incompatible BUT ignored, let's keep going + if (isBrowserOverridden) { + logger.debug('this browser is NOT supported but has been manually overridden ! ⚠️') + return + } + + // If incompatible, NOT overridden AND NOT already on the warning page, + // redirect to the unsupported warning page + if (window.location.pathname.indexOf(redirectPath) === -1) { + window.location = generateUrl(redirectPath) + } +} diff --git a/core/src/views/UnsupportedBrowser.vue b/core/src/views/UnsupportedBrowser.vue new file mode 100644 index 00000000000..ef2a33ca213 --- /dev/null +++ b/core/src/views/UnsupportedBrowser.vue @@ -0,0 +1,191 @@ + <!-- + - @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com> + - + - @author John Molakvoæ <skjnldsv@protonmail.com> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + - + --> +<template> + <div class="content-unsupported-browser guest-box"> + <NcEmptyContent> + {{ t('core', 'This browser is not supported') }} + <template #icon> + <Web /> + </template> + <template #action> + <div> + <h2> + {{ t('core', 'Please upgrade to a more recent browser') }} + </h2> + <NcButton class="content-unsupported-browser__continue" type="primary" @click="forceBrowsing"> + {{ t('core', 'Continue with this outdated browser') }} + </NcButton> + </div> + + <ul class="content-unsupported-browser__list"> + <h3>{{ t('core', 'Supported versions') }}</h3> + <li v-for="browser in formattedBrowsersList" :key="browser"> + {{ browser }} + </li> + </ul> + </template> + </NcEmptyContent> + </div> +</template> + +<script> +import { generateUrl } from '@nextcloud/router' +import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent' +import Web from 'vue-material-design-icons/Web' + +import { browserStorageKey } from '../utils/RedirectUnsupportedBrowsers.js' +import { supportedBrowsers } from '../services/BrowsersListService.js' +import browserStorage from '../services/BrowserStorageService.js' +import logger from '../services/LoggerService.js' + +logger.debug('Supported browsers', { supportedBrowsers }) + +export default { + name: 'UnsupportedBrowser', + components: { + Web, + NcButton, + NcEmptyContent, + }, + + data() { + return { + agents: {}, + } + }, + + computed: { + isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + }, + + /** + * Filter out or include mobile/desktop browsers depending + * on the current user platform/device + */ + filteredSupportedBrowsers() { + return supportedBrowsers.filter(browser => { + if (!browser) { + return false + } + + if (this.isMobile) { + return this.isMobileBrowser(browser) + } + return !this.isMobileBrowser(browser) + }) + }, + + formattedBrowsersList() { + const list = {} + + // supportedBrowsers is generated by webpack at compilation time + this.filteredSupportedBrowsers.forEach(browser => { + const [id, version] = browser.split(' ') + if (!list[id] || list[id] < parseFloat(version, 10)) { + list[id] = parseFloat(version, 10) + } + }) + + return Object.keys(list).map(id => { + if (!this.agents[id]?.browser) { + return null + } + + const version = list[id] + const name = this.agents[id]?.browser + return this.t('core', '{name} version {version} and above', { + name, version, + }) + }).filter(entry => entry !== null) + }, + }, + + async beforeMount() { + // Dynamic load big list of user agents + // eslint-disable-next-line node/no-extraneous-import + const { agents } = await import('caniuse-lite') + this.agents = agents + }, + + methods: { + t, + n, + + // Set the flag allowing this browser and redirect to home + forceBrowsing() { + browserStorage.setItem(browserStorageKey, true) + window.location = generateUrl('/') + }, + + /** + * Detect if the browserslist browser is a mobile one + * https://github.com/browserslist/browserslist#query-composition + * + * @param {string} browser a valid browserlist browser. e.g `and_chr 90` + */ + isMobileBrowser(browser) { + browser = browser.toLowerCase() + return browser.includes('and_') + || browser.includes('android') + || browser.includes('ios_') + || browser.includes('mobile') + || browser.includes('_mob') + || browser.includes('samsung') + }, + }, +} +</script> + +<style lang="scss" scoped> +.content-unsupported-browser { + display: flex; + justify-content: center; + width: 400px; + max-width: 90vw; + margin: auto; + padding: 30px; + + .empty-content { + margin: 0; + &::v-deep .empty-content__icon { + opacity: 1; + } + } + + &__continue { + display: block; + margin: 20px auto; + } + + &__list { + margin-top: 60px; + margin-bottom: 30px; + li { + text-align: left; + } + } +} + +</style> diff --git a/core/templates/unsupportedbrowser.php b/core/templates/unsupportedbrowser.php new file mode 100644 index 00000000000..54547e204f3 --- /dev/null +++ b/core/templates/unsupportedbrowser.php @@ -0,0 +1 @@ +<div id="unsupported-browser"></div> |