diff options
-rw-r--r-- | core/css/header.scss | 201 | ||||
-rw-r--r-- | core/js/core.json | 3 | ||||
-rw-r--r-- | core/js/js.js | 15 | ||||
-rw-r--r-- | core/js/l10n.js | 10 | ||||
-rw-r--r-- | core/js/tests/specs/l10nSpec.js | 5 | ||||
-rw-r--r-- | core/js/tests/specs/setupchecksSpec.js | 60 | ||||
-rw-r--r-- | core/templates/layout.user.php | 123 | ||||
-rw-r--r-- | lib/private/TemplateLayout.php | 2 | ||||
-rw-r--r-- | lib/private/legacy/app.php | 83 | ||||
-rw-r--r-- | settings/js/apps.js | 105 | ||||
-rw-r--r-- | settings/templates/apps.php | 1 | ||||
-rw-r--r-- | tests/Settings/Controller/CheckSetupControllerTest.php | 1 |
12 files changed, 500 insertions, 109 deletions
diff --git a/core/css/header.scss b/core/css/header.scss index 2b73937a3c4..2f0c1522b0b 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -109,7 +109,7 @@ height: 34px; } .header-appname-container { - display: inline-block; + display: none; padding-top: 22px; padding-right: 10px; flex-shrink: 0; @@ -181,29 +181,31 @@ font-size: 16px; font-weight: 300; margin: 0; - margin-top: -27px; + margin-top: -26px; padding: 7px 0 7px 5px; vertical-align: middle; } - - /* do not show menu toggle on public share links as there is no menu */ #body-public #header .icon-caret { display: none; } /* NAVIGATION --------------------------------------------------------------- */ +nav { + margin-top: auto; +} + #navigation { - position: fixed; + position: relative; top: 45px; - left: 10px; + left: -100%; width: 265px; max-height: 85%; margin-top: 0; padding-bottom: 10px; - background-color: rgba(255, 255, 255, 0.97); - box-shadow: 0 1px 10px rgba(150, 150, 150, 0.75); + background-color: rgba(255, 255, 255, .97); + box-shadow: 0 1px 10px rgba(150, 150, 150, .75); border-radius: 3px; border-top-left-radius: 0; border-top-right-radius: 0; @@ -212,7 +214,48 @@ z-index: 2000; &:after { left: 47%; + bottom: 100%; + border: solid transparent; + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(0, 0, 0, 0); + border-bottom-color: rgba(255, 255, 255, .97); + border-width: 9px; + margin-left: -9px; } +} + +/* arrow look */ + +#expanddiv:after { + bottom: 100%; + border: solid transparent; + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(0, 0, 0, 0); + border-bottom-color: rgba(255, 255, 255, .97); + border-width: 10px; + margin-left: -10px; +} + +/* position of dropdown arrow */ + +#navigation:after { + left: 242px; +} + +#expanddiv:after { + right: 15px; +} + +#navigation { + box-sizing: border-box; * { box-sizing: border-box; } @@ -307,6 +350,9 @@ #apps { max-height: calc(100vh - 100px); overflow: auto; + .in-header { + display: none; + } } /* USER MENU -----------------------------------------------------------------*/ @@ -375,7 +421,7 @@ z-index: 2000; display: none; background: rgb(255, 255, 255); - box-shadow: 0 1px 10px rgba(150, 150, 150, 0.75); + box-shadow: 0 1px 10px rgba(150, 150, 150, .75); border-radius: 3px; border-top-left-radius: 0; border-top-right-radius: 0; @@ -405,3 +451,140 @@ } } } + +/* do not show display name when profile picture is present */ + +#header { + .avatardiv.avatardiv-shown + #expandDisplayName { + display: none; + } + #expand { + display: block; + } +} + +#appmenu { + display: inline-block; + width: auto; + clear: both; + height: 44px; + + li { + float: left; + display: inline-block; + vertical-align: top !important; + height: 20px; + padding: 12px; + + a { + opacity: 0.6; + margin: 0; + text-align: center; + vertical-align: top !important; + position: relative; + height: 44px; + } + } + + li:hover a, + li a.active { + opacity: 1; + + } + + li img, + .icon-more-white { + display: inline-block; + width: 20px; + height: 20px; + } + + li span { + display: none; + position: absolute; + overflow: visible; + background-color: rgba(255, 255, 255, .97); + white-space: nowrap; + border: none; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + border-top-left-radius: 0; + border-top-right-radius: 0; + margin-top: 0; + color: rgba(0, 0, 0, .6); + width: auto; + left: 50%; + top: 31px; + transform: translateX(-50%); + padding: 4px 10px; + -webkit-filter: drop-shadow(0 0 5px rgba(150, 150, 150, .75)); + -moz-filter: drop-shadow(0 0 5px rgba(150, 150, 150, .75)); + -ms-filter: drop-shadow(0 0 5px rgba(150, 150, 150, .75)); + -o-filter: drop-shadow(0 0 5px rgba(150, 150, 150, .75)); + filter: drop-shadow(0 0 5px rgba(150, 150, 150, .75)); + } + + li:hover span { + display: inline-block; + } + + + li:hover a:before, + li a.active:before { + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border: 0 solid transparent; + border-bottom-color: white; + border-width: 10px; + transform: translateX(-50%); + left: 50%; + top: 12px; + z-index: 100; + display: block; + } + &.menu-open li:hover a:before, + &.menu-open li a.active:before, + &.menu-open li:hover span { + display: none !important; + } + + /* do not show active indicator when hovering other icons */ + &:hover li:not(:hover) a:before { + display: none; + } + + li.hidden { + display: none; + } + +} + +/* use popover menu on mobile and small screens */ +@media only screen and (max-width: 600px) { + + #header .header-appname-container { + display: inline-block !important; + } + + #appmenu { + display: none; + } + + #apps .in-header { + display: inline-block; + } + + #navigation { + position: fixed; + top: 45px; + left: 10px; + &:after { + left: 214px; + } + } + +}
\ No newline at end of file diff --git a/core/js/core.json b/core/js/core.json index d589208c828..4d1d0685007 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -12,7 +12,8 @@ "es6-promise/dist/es6-promise.js", "davclient.js/lib/client.js", "clipboard/dist/clipboard.js", - "autosize/dist/autosize.js" + "autosize/dist/autosize.js", + "DOMPurify/dist/purify.min.js" ], "libraries": [ "jquery-showpassword.js", diff --git a/core/js/js.js b/core/js/js.js index 6fd66c9c9bb..c8907cdfc90 100644 --- a/core/js/js.js +++ b/core/js/js.js @@ -1369,6 +1369,10 @@ function initCore() { * If the screen is bigger, the main menu is not a toggle any more. */ function setupMainMenu() { + + // init the more-apps menu + OC.registerMenu($('#more-apps'), $('#navigation')); + // toggle the navigation var $toggle = $('#header .header-appname-container'); var $navigation = $('#navigation'); @@ -1438,13 +1442,20 @@ function initCore() { // move triangle of apps dropdown to align with app name triangle // 2 is the additional offset between the triangles if($('#navigation').length) { - $('#header #nextcloud + .menutoggle').one('click', function(){ + $('#header #nextcloud + .menutoggle').on('click', function(){ + $('#menu-css-helper').remove(); var caretPosition = $('.header-appname + .icon-caret').offset().left - 2; if(caretPosition > 255) { // if the app name is longer than the menu, just put the triangle in the middle return; } else { - $('head').append('<style>#navigation:after { left: '+ caretPosition +'px; }</style>'); + $('head').append('<style id="menu-css-helper">#navigation:after { left: '+ caretPosition +'px; }</style>'); + } + }); + $('#header #appmenu .menutoggle').on('click', function() { + $('#appmenu').toggleClass('menu-open'); + if($('#appmenu').is(':visible')) { + $('#menu-css-helper').remove(); } }); } diff --git a/core/js/l10n.js b/core/js/l10n.js index 43cfc7e820f..77f771a20b3 100644 --- a/core/js/l10n.js +++ b/core/js/l10n.js @@ -155,12 +155,12 @@ OC.L10N = { var r = vars[b]; if(typeof r === 'string' || typeof r === 'number') { if(allOptions.escape) { - return escapeHTML(r); + return DOMPurify.sanitize(escapeHTML(r)); } else { - return r; + return DOMPurify.sanitize(r); } } else { - return a; + return DOMPurify.sanitize(a); } } ); @@ -173,9 +173,9 @@ OC.L10N = { } if(typeof vars === 'object' || count !== undefined ) { - return _build(translation, vars, count); + return DOMPurify.sanitize(_build(translation, vars, count)); } else { - return translation; + return DOMPurify.sanitize(translation); } }, diff --git a/core/js/tests/specs/l10nSpec.js b/core/js/tests/specs/l10nSpec.js index 064b27aa34a..3dd1fa268ef 100644 --- a/core/js/tests/specs/l10nSpec.js +++ b/core/js/tests/specs/l10nSpec.js @@ -53,6 +53,11 @@ describe('OC.L10N tests', function() { t(TEST_APP, 'Hello {name}', {name: '<strong>Steve</strong>'}, null, {escape: false}) ).toEqual('Hello <strong>Steve</strong>'); }); + it('uses DOMPurify to escape the text', function() { + expect( + t(TEST_APP, '<strong>These are your search results<script>alert(1)</script></strong>', null, {escape: false}) + ).toEqual('<strong>These are your search results</strong>'); + }); it('keeps old texts when registering existing bundle', function() { OC.L10N.register(TEST_APP, { 'sunny': 'sonnig', diff --git a/core/js/tests/specs/setupchecksSpec.js b/core/js/tests/specs/setupchecksSpec.js index 1ee16a7af81..937084aaa24 100644 --- a/core/js/tests/specs/setupchecksSpec.js +++ b/core/js/tests/specs/setupchecksSpec.js @@ -68,7 +68,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'Your web server is not set up properly to resolve "/.well-known/caldav/". Further information can be found in our <a target="_blank" rel="noreferrer" href="http://example.org/admin-setup-well-known-URL">documentation</a>.', + msg: 'Your web server is not set up properly to resolve "/.well-known/caldav/". Further information can be found in our <a href="http://example.org/admin-setup-well-known-URL" rel="noreferrer">documentation</a>.', type: OC.SetupChecks.MESSAGE_TYPE_INFO }]); done(); @@ -156,6 +156,7 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); @@ -165,7 +166,7 @@ describe('OC.SetupChecks tests', function() { msg: 'This server has no working Internet connection: Multiple endpoints could not be reached. This means that some of the features like mounting external storage, notifications about updates or installation of third-party apps will not work. Accessing files remotely and sending of notification emails might not work, either. We suggest to enable Internet connection for this server if you want to have all features.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }, { - msg: 'No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a target="_blank" rel="noreferrer" href="https://doc.owncloud.org/server/go.php?to=admin-performance">documentation</a>.', + msg: 'No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a href="https://doc.owncloud.org/server/go.php?to=admin-performance" rel="noreferrer">documentation</a>.', type: OC.SetupChecks.MESSAGE_TYPE_INFO }]); done(); @@ -188,6 +189,7 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); @@ -198,7 +200,7 @@ describe('OC.SetupChecks tests', function() { type: OC.SetupChecks.MESSAGE_TYPE_WARNING }, { - msg: 'No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a target="_blank" rel="noreferrer" href="https://doc.owncloud.org/server/go.php?to=admin-performance">documentation</a>.', + msg: 'No memory cache has been configured. To enhance your performance please configure a memcache if available. Further information can be found in our <a href="https://doc.owncloud.org/server/go.php?to=admin-performance" rel="noreferrer">documentation</a>.', type: OC.SetupChecks.MESSAGE_TYPE_INFO }]); done(); @@ -221,6 +223,7 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); @@ -252,12 +255,13 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: '/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our <a target="_blank" rel="noreferrer" href="https://docs.owncloud.org/myDocs.html">documentation</a>.', + msg: '/dev/urandom is not readable by PHP which is highly discouraged for security reasons. Further information can be found in our <a href="https://docs.owncloud.org/myDocs.html" rel="noreferrer">documentation</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -281,12 +285,13 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: false, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'Memcached is configured as distributed cache, but the wrong PHP module "memcache" is installed. \\OC\\Memcache\\Memcached only supports "memcached" and not "memcache". See the <a target="_blank" rel="noreferrer" href="https://code.google.com/p/memcached/wiki/PHPClientComparison">memcached wiki about both modules</a>.', + msg: 'Memcached is configured as distributed cache, but the wrong PHP module "memcache" is installed. \\OC\\Memcache\\Memcached only supports "memcached" and not "memcache". See the <a href="https://code.google.com/p/memcached/wiki/PHPClientComparison" rel="noreferrer">memcached wiki about both modules</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -310,12 +315,43 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The reverse proxy headers configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If you are not accessing Nextcloud from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to Nextcloud. Further information can be found in our <a target="_blank" rel="noreferrer" href="https://docs.owncloud.org/foo/bar.html">documentation</a>.', + msg: 'The reverse proxy headers configuration is incorrect, or you are accessing Nextcloud from a trusted proxy. If you are not accessing Nextcloud from a trusted proxy, this is a security issue and can allow an attacker to spoof their IP address as visible to Nextcloud. Further information can be found in our <a href="https://docs.owncloud.org/foo/bar.html" rel="noreferrer">documentation</a>.', + type: OC.SetupChecks.MESSAGE_TYPE_WARNING + }]); + done(); + }); + }); + + it('should return an error if set_time_limit is unavailable', function(done) { + var async = OC.SetupChecks.checkSetup(); + + suite.server.requests[0].respond( + 200, + { + 'Content-Type': 'application/json', + }, + JSON.stringify({ + isUrandomAvailable: true, + serverHasInternetConnection: true, + isMemcacheConfigured: true, + forwardedForHeadersWorking: true, + reverseProxyDocs: 'https://docs.owncloud.org/foo/bar.html', + isCorrectMemcachedPHPModuleInstalled: true, + hasPassedCodeIntegrityCheck: true, + isOpcacheProperlySetup: true, + isSettimelimitAvailable: false + }) + ); + + async.done(function( data, s, x ){ + expect(data).toEqual([{ + msg: 'The PHP function "set_time_limit" is not available. This could result in scripts being halted mid-execution, breaking your installation. We strongly recommend enabling this function.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -360,12 +396,13 @@ describe('OC.SetupChecks tests', function() { isCorrectMemcachedPHPModuleInstalled: true, hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: true, + isSettimelimitAvailable: true }) ); async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'You are currently running PHP 5.4.0. We encourage you to upgrade your PHP version to take advantage of <a target="_blank" rel="noreferrer" href="https://secure.php.net/supported-versions.php">performance and security updates provided by the PHP Group</a> as soon as your distribution supports it.', + msg: 'You are currently running PHP 5.4.0. We encourage you to upgrade your PHP version to take advantage of <a href="https://secure.php.net/supported-versions.php" rel="noreferrer">performance and security updates provided by the PHP Group</a> as soon as your distribution supports it.', type: OC.SetupChecks.MESSAGE_TYPE_INFO }]); done(); @@ -390,12 +427,13 @@ describe('OC.SetupChecks tests', function() { hasPassedCodeIntegrityCheck: true, isOpcacheProperlySetup: false, phpOpcacheDocumentation: 'https://example.org/link/to/doc', + isSettimelimitAvailable: true }) ); async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The PHP Opcache is not properly configured. <a target="_blank" rel="noreferrer" href="https://example.org/link/to/doc">For better performance we recommend ↗</a> to use following settings in the <code>php.ini</code>:' + "<pre><code>opcache.enable=On\nopcache.enable_cli=1\nopcache.interned_strings_buffer=8\nopcache.max_accelerated_files=10000\nopcache.memory_consumption=128\nopcache.save_comments=1\nopcache.revalidate_freq=1</code></pre>", + msg: 'The PHP Opcache is not properly configured. <a href="https://example.org/link/to/doc" rel="noreferrer">For better performance we recommend ↗</a> to use following settings in the <code>php.ini</code>:' + "<pre><code>opcache.enable=On\nopcache.enable_cli=1\nopcache.interned_strings_buffer=8\nopcache.max_accelerated_files=10000\nopcache.memory_consumption=128\nopcache.save_comments=1\nopcache.revalidate_freq=1</code></pre>", type: OC.SetupChecks.MESSAGE_TYPE_INFO }]); done(); @@ -579,7 +617,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="http://localhost/index.php/settings/admin/tips-tricks" rel="noreferrer">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a rel="noreferrer" href="http://localhost/index.php/settings/admin/tips-tricks">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -604,7 +642,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="http://localhost/index.php/settings/admin/tips-tricks" rel="noreferrer">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a rel="noreferrer" href="http://localhost/index.php/settings/admin/tips-tricks">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); @@ -629,7 +667,7 @@ describe('OC.SetupChecks tests', function() { async.done(function( data, s, x ){ expect(data).toEqual([{ - msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a href="http://localhost/index.php/settings/admin/tips-tricks" rel="noreferrer">security tips</a>.', + msg: 'The "Strict-Transport-Security" HTTP header is not configured to at least "15552000" seconds. For enhanced security we recommend enabling HSTS as described in our <a rel="noreferrer" href="http://localhost/index.php/settings/admin/tips-tricks">security tips</a>.', type: OC.SetupChecks.MESSAGE_TYPE_WARNING }]); done(); diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index e9a9b042e07..3cfb88bf423 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -58,9 +58,88 @@ </h1> <div class="icon-caret"></div> </a> + + <div id="appmenu"> + <ul> + <?php $headerIconCount = 8; ?> + <?php foreach($_['headernavigation'] as $entry): ?> + <li data-id="<?php p($entry['id']); ?>"> + <a href="<?php print_unescaped($entry['href']); ?>" tabindex="3" + <?php if( $entry['active'] ): ?> class="active"<?php endif; ?>> + <img src="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon" /> + <div class="icon-loading-dark" style="display:none;"></div> + <span> + <?php p($entry['name']); ?> + </span> + </a> + </li> + <?php endforeach; ?> + <li id="more-apps" class="menutoggle<?php if (!(count($_['navigation']) > $headerIconCount || (OC_User::isAdminUser(OC_User::getUser()) && count($_['navigation'])>=$headerIconCount))): ?> hidden<?php endif; ?>"> + <a href="#"> + <div class="icon-more-white"></div> + <span><?php p($l->t('More apps')); ?></span> + </a> + </li> + <?php if(OC_User::isAdminUser(OC_User::getUser())): ?> + <li id="apps-management" <?php if(count($_['navigation'])>$headerIconCount-1): ?>class="hidden"<?php endif; ?>> + <a href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')); ?>" tabindex="4" + <?php if( $_['appsmanagement_active'] ): ?> class="active"<?php endif; ?>> + <img src="<?php print_unescaped(image_path('settings', 'apps.svg') . '?v=' . $_['versionHash']); ?>" /> + <div class="icon-loading-dark" style="display:none;"></div> + <span><?php p($l->t('Apps')); ?></span> + </a> + </li> + <?php endif; ?> + </ul> + </div> + + <nav role="navigation"><div id="navigation"> + <div id="apps"> + <ul> + <?php foreach($_['navigation'] as $entry): ?> + <?php if($entry['showInHeader']): ?> + <li data-id="<?php p($entry['id']); ?>" class="in-header"> + <?php else: ?> + <li data-id="<?php p($entry['id']); ?>"> + <?php endif; ?> + <a href="<?php print_unescaped($entry['href']); ?>" tabindex="3" + <?php if( $entry['active'] ): ?> class="active"<?php endif; ?>> + <svg width="32" height="32" viewBox="0 0 32 32"> + <defs><filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs> + <image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon"></image> + </svg> + <div class="icon-loading-dark" style="display:none;"></div> + <span> + <?php p($entry['name']); ?> + </span> + </a> + </li> + <?php endforeach; ?> + <?php + /* show "More apps" link to app administration directly in app navigation, as last entry */ + if(OC_User::isAdminUser(OC_User::getUser())): + ?> + <li id="apps-management"> + <a href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')); ?>" tabindex="4" + <?php if( $_['appsmanagement_active'] ): ?> class="active"<?php endif; ?>> + <svg width="32" height="32" viewBox="0 0 32 32" class="app-icon"> + <defs><filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs> + <image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="<?php print_unescaped(image_path('settings', 'apps.svg') . '?v=' . $_['versionHash']); ?>"></image> + </svg> + <div class="icon-loading-dark" style="display:none;"></div> + <span> + <?php p($l->t('Apps')); ?> + </span> + </a> + </li> + <?php endif; ?> + + </ul> + </div> + </div></nav> + </div> - <div id="logo-claim" style="display:none;"><?php p($theme->getLogoClaim()); ?></div> <div id="header-right"> <form class="searchbox" action="#" method="post" role="search" novalidate> <label for="searchbox" class="hidden-visually"> @@ -102,52 +181,12 @@ </a> </li> </ul> + </div> </div> </div> </div></header> - <nav role="navigation"><div id="navigation"> - <div id="apps"> - <ul> - <?php foreach($_['navigation'] as $entry): ?> - <li data-id="<?php p($entry['id']); ?>"> - <a href="<?php print_unescaped($entry['href']); ?>" tabindex="3" - <?php if( $entry['active'] ): ?> class="active"<?php endif; ?>> - <svg width="32" height="32" viewBox="0 0 32 32"> - <defs><filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs> - <image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="<?php print_unescaped($entry['icon'] . '?v=' . $_['versionHash']); ?>" class="app-icon"></image> - </svg> - <div class="icon-loading-dark" style="display:none;"></div> - <span> - <?php p($entry['name']); ?> - </span> - </a> - </li> - <?php endforeach; ?> - <?php - /* show "More apps" link to app administration directly in app navigation, as last entry */ - if(OC_User::isAdminUser(OC_User::getUser())): - ?> - <li id="apps-management"> - <a href="<?php print_unescaped(\OC::$server->getURLGenerator()->linkToRoute('settings.AppSettings.viewApps')); ?>" tabindex="4" - <?php if( $_['appsmanagement_active'] ): ?> class="active"<?php endif; ?>> - <svg width="32" height="32" viewBox="0 0 32 32" class="app-icon"> - <defs><filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0"></feColorMatrix></filter></defs> - <image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="<?php print_unescaped(image_path('settings', 'apps.svg') . '?v=' . $_['versionHash']); ?>"></image> - </svg> - <div class="icon-loading-dark" style="display:none;"></div> - <span> - <?php p($l->t('Apps')); ?> - </span> - </a> - </li> - <?php endif; ?> - - </ul> - </div> - </div></nav> - <div id="sudo-login-background" class="hidden"></div> <form id="sudo-login-form" class="hidden"> <?php p($l->t('This action requires you to confirm your password:')); ?><br> diff --git a/lib/private/TemplateLayout.php b/lib/private/TemplateLayout.php index ccd53c9cafa..3f8c75adc84 100644 --- a/lib/private/TemplateLayout.php +++ b/lib/private/TemplateLayout.php @@ -76,6 +76,8 @@ class TemplateLayout extends \OC_Template { $this->assign( 'appid', $appId ); $navigation = \OC_App::getNavigation(); $this->assign( 'navigation', $navigation); + $navigation = \OC_App::getHeaderNavigation(); + $this->assign( 'headernavigation', $navigation); $settingsNavigation = \OC_App::getSettingsNavigation(); $this->assign( 'settingsnavigation', $settingsNavigation); foreach($navigation as $entry) { diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index f89f32f069a..c82d620882d 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -529,25 +529,76 @@ class OC_App { // This is private as well. It simply works, so don't ask for more details private static function proceedNavigation($list) { + $headerIconCount = 8; + if(OC_User::isAdminUser(OC_User::getUser())) { + $headerIconCount--; + } + usort($list, function($a, $b) { + if (isset($a['order']) && isset($b['order'])) { + return ($a['order'] < $b['order']) ? -1 : 1; + } else if (isset($a['order']) || isset($b['order'])) { + return isset($a['order']) ? -1 : 1; + } else { + return ($a['name'] < $b['name']) ? -1 : 1; + } + }); + + $activeAppIndex = -1; $activeApp = OC::$server->getNavigationManager()->getActiveEntry(); - foreach ($list as &$navEntry) { + foreach ($list as $index => &$navEntry) { if ($navEntry['id'] == $activeApp) { $navEntry['active'] = true; + $activeAppIndex = $index; } else { $navEntry['active'] = false; } } unset($navEntry); - usort($list, function($a, $b) { - if (isset($a['order']) && isset($b['order'])) { - return ($a['order'] < $b['order']) ? -1 : 1; - } else if (isset($a['order']) || isset($b['order'])) { - return isset($a['order']) ? -1 : 1; + if($activeAppIndex > ($headerIconCount-1)) { + $active = $list[$activeAppIndex]; + $lastInHeader = $list[$headerIconCount-1]; + $list[$headerIconCount-1] = $active; + $list[$activeAppIndex] = $lastInHeader; + } + + foreach ($list as $index => &$navEntry) { + $navEntry['showInHeader'] = false; + if($index < $headerIconCount) { + $navEntry['showInHeader'] = true; + } + } + + + + return $list; + } + + public static function proceedAppNavigation($entries) { + $headerIconCount = 8; + if(OC_User::isAdminUser(OC_User::getUser())) { + $headerIconCount--; + } + $activeAppIndex = -1; + $list = self::proceedNavigation($entries); + + $activeApp = OC::$server->getNavigationManager()->getActiveEntry(); + foreach ($list as $index => &$navEntry) { + if ($navEntry['id'] == $activeApp) { + $navEntry['active'] = true; + $activeAppIndex = $index; } else { - return ($a['name'] < $b['name']) ? -1 : 1; + $navEntry['active'] = false; } - }); + } + // move active item to last position + if($activeAppIndex > ($headerIconCount-1)) { + $active = $list[$activeAppIndex]; + $lastInHeader = $list[$headerIconCount-1]; + $list[$headerIconCount-1] = $active; + $list[$activeAppIndex] = $lastInHeader; + } + $list = array_slice($list, 0, $headerIconCount); return $list; } @@ -742,6 +793,22 @@ class OC_App { } /** + * Returns the navigation inside the header bar + * + * @return array + * + * This function returns an array containing all entries added. The + * entries are sorted by the key 'order' ascending. Additional to the keys + * given for each app the following keys exist: + * - active: boolean, signals if the user is on this navigation entry + */ + public static function getHeaderNavigation() { + $entries = OC::$server->getNavigationManager()->getAll(); + $navigation = self::proceedAppNavigation($entries); + return $navigation; + } + + /** * get the id of loaded app * * @return string diff --git a/settings/js/apps.js b/settings/js/apps.js index b73b4a35b3f..8be18c4e9c0 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -451,22 +451,39 @@ OC.Settings.Apps = OC.Settings.Apps || { rebuildNavigation: function() { $.getJSON(OC.filePath('settings', 'ajax', 'navigationdetect.php')).done(function(response){ - if(response.status === 'success'){ - var idsToKeep = {}; - var navEntries=response.nav_entries; + if(response.status === 'success') { + var addedApps = {}; + var navEntries = response.nav_entries; var container = $('#apps ul'); - for(var i=0; i< navEntries.length; i++){ + + // remove disabled apps + for (var i = 0; i < navEntries.length; i++) { var entry = navEntries[i]; - idsToKeep[entry.id] = true; + if(container.children('li[data-id="' + entry.id + '"]').length === 0) { + addedApps[entry.id] = true; + } + } + container.children('li[data-id]').each(function (index, el) { + var id = $(el).data('id'); + // remove all apps that are not in the correct order + if ((navEntries[index] && navEntries[index].id !== $(el).data('id'))) { + $(el).remove(); + $('#appmenu li[data-id='+id+']').remove(); + } + }); - if(container.children('li[data-id="'+entry.id+'"]').length === 0){ - var li=$('<li></li>'); + var previousEntry; + // add enabled apps to #navigation and #appmenu + for (var i = 0; i < navEntries.length; i++) { + var entry = navEntries[i]; + if (container.children('li[data-id="' + entry.id + '"]').length === 0) { + var li = $('<li></li>'); li.attr('data-id', entry.id); var img = '<svg width="32" height="32" viewBox="0 0 32 32">'; img += '<defs><filter id="invert"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>'; img += '<image x="0" y="0" width="32" height="32" preserveAspectRatio="xMinYMin meet" filter="url(#invert)" xlink:href="' + entry.icon + '" class="app-icon" /></svg>'; - var a=$('<a></a>').attr('href', entry.href); - var filename=$('<span></span>'); + var a = $('<a></a>').attr('href', entry.href); + var filename = $('<span></span>'); var loading = $('<div class="icon-loading-dark"></div>').css('display', 'none'); filename.text(entry.name); a.prepend(filename); @@ -474,33 +491,61 @@ OC.Settings.Apps = OC.Settings.Apps || { a.prepend(img); li.append(a); - // append the new app as last item in the list - // which is the "add apps" entry with the id - // #apps-management - $('#apps-management').before(li); - - // scroll the app navigation down - // so the newly added app is seen - $('#navigation').animate({ - scrollTop: $('#navigation').height() - }, 'slow'); + $('#navigation li[data-id=' + previousEntry.id + ']').after(li); // draw attention to the newly added app entry // by flashing it twice - $('#header .menutoggle') - .animate({opacity: 0.5}) - .animate({opacity: 1}) - .animate({opacity: 0.5}) - .animate({opacity: 1}) - .animate({opacity: 0.75}); + if(addedApps[entry.id]) { + $('#header .menutoggle') + .animate({opacity: 0.5}) + .animate({opacity: 1}) + .animate({opacity: 0.5}) + .animate({opacity: 1}) + .animate({opacity: 0.75}); + } } - } - container.children('li[data-id]').each(function(index, el) { - if (!idsToKeep[$(el).data('id')]) { - $(el).remove(); + if ($('#appmenu ul').children('li[data-id="' + entry.id + '"]').length === 0) { + // add apps to #appmenu until it is full + if ($('#appmenu li').not('.hidden').length < 8) { + var li = $('<li></li>'); + li.attr('data-id', entry.id); + var img = '<img src="' + entry.icon + '" class="app-icon">'; + var a = $('<a></a>').attr('href', entry.href); + var filename = $('<span></span>'); + var loading = $('<div class="icon-loading-dark"></div>').css('display', 'none'); + filename.text(entry.name); + a.prepend(filename); + a.prepend(loading); + a.prepend(img); + li.append(a); + $('#appmenu li[data-id='+ previousEntry.id+']').after(li); + if(addedApps[entry.id]) { + li.animate({opacity: 0.5}) + .animate({opacity: 1}) + .animate({opacity: 0.5}) + .animate({opacity: 1}); + } + } } - }); + previousEntry = entry; + // do not show apps from #appmenu in #navigation + if(i < 7) { + $('#navigation li').eq(i).addClass('in-header'); + } else { + $('#navigation li').eq(i).removeClass('in-header'); + } + } + + + + if (navEntries.length > 7) { + $('#more-apps').show(); + $('#apps-management').hide(); + } else { + $('#more-apps').hide(); + $('#apps-management').show(); + } } }); }, diff --git a/settings/templates/apps.php b/settings/templates/apps.php index 80689237e60..99d648c6284 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -5,7 +5,6 @@ vendor_script( [ 'handlebars/handlebars', 'marked/marked.min', - 'DOMPurify/dist/purify.min', ] ); script( diff --git a/tests/Settings/Controller/CheckSetupControllerTest.php b/tests/Settings/Controller/CheckSetupControllerTest.php index e600f7e5e9c..d9ba7d43672 100644 --- a/tests/Settings/Controller/CheckSetupControllerTest.php +++ b/tests/Settings/Controller/CheckSetupControllerTest.php @@ -339,6 +339,7 @@ class CheckSetupControllerTest extends TestCase { 'codeIntegrityCheckerDocumentation' => 'http://doc.owncloud.org/server/go.php?to=admin-code-integrity', 'isOpcacheProperlySetup' => false, 'phpOpcacheDocumentation' => 'http://doc.owncloud.org/server/go.php?to=admin-php-opcache', + 'isSettimelimitAvailable' => true, ] ); $this->assertEquals($expected, $this->checkSetupController->check()); |