aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
m---------3rdparty0
-rw-r--r--apps/encryption/js/encryption.js8
-rw-r--r--apps/encryption/js/settings-personal.js8
-rw-r--r--apps/encryption/lib/migration.php6
-rw-r--r--apps/encryption/tests/lib/MigrationTest.php34
-rw-r--r--apps/files/appinfo/application.php9
-rw-r--r--apps/files/appinfo/routes.php9
-rw-r--r--apps/files/css/detailsView.css55
-rw-r--r--apps/files/css/files.css5
-rw-r--r--apps/files/index.php6
-rw-r--r--apps/files/js/detailfileinfoview.js96
-rw-r--r--apps/files/js/detailsview.js251
-rw-r--r--apps/files/js/detailtabview.js136
-rw-r--r--apps/files/js/filelist.js128
-rw-r--r--apps/files/js/mainfileinfodetailview.js98
-rw-r--r--apps/files/lib/capabilities.php37
-rw-r--r--apps/files/tests/js/detailsviewSpec.js105
-rw-r--r--apps/files/tests/js/favoritespluginspec.js2
-rw-r--r--apps/files/tests/js/filelistSpec.js46
-rw-r--r--apps/files/tests/js/mainfileinfodetailviewSpec.js104
-rw-r--r--apps/files_external/appinfo/application.php2
-rw-r--r--apps/files_external/appinfo/routes.php2
-rw-r--r--apps/files_external/lib/amazons3.php5
-rw-r--r--apps/files_external/lib/config.php12
-rw-r--r--apps/files_external/lib/dropbox.php5
-rw-r--r--apps/files_external/lib/google.php5
-rw-r--r--apps/files_external/lib/sftp.php10
-rw-r--r--apps/files_external/lib/sftp_key.php8
-rw-r--r--apps/files_external/lib/streamwrapper.php5
-rw-r--r--apps/files_external/lib/swift.php4
-rw-r--r--apps/files_sharing/appinfo/app.php1
-rw-r--r--apps/files_sharing/appinfo/application.php9
-rw-r--r--apps/files_sharing/appinfo/routes.php5
-rw-r--r--apps/files_sharing/css/sharetabview.css3
-rw-r--r--apps/files_sharing/js/public.js3
-rw-r--r--apps/files_sharing/js/share.js4
-rw-r--r--apps/files_sharing/js/sharetabview.js67
-rw-r--r--apps/files_sharing/lib/cache.php4
-rw-r--r--apps/files_sharing/lib/capabilities.php32
-rw-r--r--apps/files_sharing/lib/share/file.php26
-rw-r--r--apps/files_sharing/lib/sharedstorage.php4
-rw-r--r--apps/files_sharing/tests/capabilities.php7
-rw-r--r--apps/files_sharing/tests/js/appSpec.js2
-rw-r--r--apps/files_sharing/tests/sharedstorage.php39
-rw-r--r--apps/files_trashbin/appinfo/application.php37
-rw-r--r--apps/files_trashbin/appinfo/routes.php11
-rw-r--r--apps/files_trashbin/command/cleanup.php2
-rw-r--r--apps/files_trashbin/lib/capabilities.php22
-rw-r--r--apps/files_trashbin/lib/helper.php2
-rw-r--r--apps/files_trashbin/tests/command/cleanuptest.php2
-rw-r--r--apps/files_versions/appinfo/application.php37
-rw-r--r--apps/files_versions/appinfo/routes.php8
-rw-r--r--apps/files_versions/lib/capabilities.php25
-rw-r--r--apps/user_ldap/appinfo/update.php26
-rw-r--r--apps/user_ldap/tests/integration/lib/integrationtestuserhome.php159
-rw-r--r--apps/user_ldap/user_ldap.php7
-rw-r--r--bower.json3
-rw-r--r--core/css/apps.css79
-rw-r--r--core/css/mobile.css4
-rw-r--r--core/js/apps.js20
-rw-r--r--core/js/core.json1
-rw-r--r--core/js/js.js4
-rw-r--r--core/js/oc-backbone.js12
-rw-r--r--core/js/setupchecks.js5
-rw-r--r--core/js/tests/specHelper.js2
-rw-r--r--core/js/tests/specs/setupchecksSpec.js17
-rw-r--r--core/vendor/.gitignore3
-rw-r--r--core/vendor/backbone/.bower.json33
-rw-r--r--core/vendor/backbone/LICENSE22
-rw-r--r--core/vendor/backbone/backbone.js1873
-rw-r--r--db_structure.xml12
-rw-r--r--lib/base.php2
-rw-r--r--lib/private/api.php113
-rw-r--r--lib/private/app.php13
-rw-r--r--lib/private/appframework/dependencyinjection/dicontainer.php18
-rw-r--r--lib/private/capabilitiesmanager.php64
-rw-r--r--lib/private/connector/sabre/file.php3
-rw-r--r--lib/private/db/querybuilder/querybuilder.php42
-rw-r--r--lib/private/defaults.php19
-rw-r--r--lib/private/files/cache/storage.php48
-rw-r--r--lib/private/files/mount/mountpoint.php7
-rw-r--r--lib/private/files/objectstore/objectstorestorage.php5
-rw-r--r--lib/private/files/storage/common.php19
-rw-r--r--lib/private/files/storage/dav.php5
-rw-r--r--lib/private/files/storage/local.php1
-rw-r--r--lib/private/files/storage/wrapper/availability.php462
-rw-r--r--lib/private/files/storage/wrapper/wrapper.php18
-rw-r--r--lib/private/files/view.php2
-rw-r--r--lib/private/installer.php4
-rw-r--r--lib/private/ocs.php145
-rw-r--r--lib/private/ocs/cloud.php9
-rw-r--r--lib/private/ocs/corecapabilities.php56
-rw-r--r--lib/private/ocs/exception.php34
-rw-r--r--lib/private/ocs/result.php45
-rw-r--r--lib/private/route/router.php6
-rw-r--r--lib/private/server.php17
-rw-r--r--lib/private/share/share.php4
-rw-r--r--lib/private/util.php8
-rw-r--r--lib/public/appframework/http/ocsresponse.php28
-rw-r--r--lib/public/appframework/iappcontainer.php7
-rw-r--r--lib/public/appframework/ocscontroller.php11
-rw-r--r--lib/public/capabilities/icapability.php46
-rw-r--r--lib/public/db/querybuilder/iquerybuilder.php9
-rw-r--r--lib/public/files/storage.php20
-rw-r--r--lib/public/iservercontainer.php1
-rw-r--r--lib/repair/cleantags.php12
-rw-r--r--lib/repair/filletags.php2
-rw-r--r--ocs/v1.php2
-rw-r--r--ocs/v2.php22
-rw-r--r--settings/application.php3
-rw-r--r--settings/controller/certificatecontroller.php34
-rw-r--r--settings/controller/checksetupcontroller.php18
-rw-r--r--settings/css/settings.css6
-rw-r--r--settings/js/admin.js7
-rw-r--r--settings/js/users/filter.js57
-rw-r--r--settings/js/users/groups.js6
-rw-r--r--settings/js/users/users.js8
-rw-r--r--settings/personal.php18
-rw-r--r--settings/templates/admin.php13
-rw-r--r--settings/templates/personal.php2
-rw-r--r--settings/templates/users/part.createuser.php3
-rw-r--r--tests/lib/appframework/controller/OCSControllerTest.php12
-rw-r--r--tests/lib/appframework/http/OCSResponseTest.php5
-rw-r--r--tests/lib/capabilitiesmanager.php164
-rw-r--r--tests/lib/db/querybuilder/querybuildertest.php98
-rw-r--r--tests/lib/files/mount/mountpoint.php21
-rw-r--r--tests/lib/files/storage/wrapper/availability.php149
-rw-r--r--tests/lib/files/view.php63
-rw-r--r--tests/lib/repair/cleantags.php34
-rw-r--r--tests/lib/server.php4
-rw-r--r--tests/lib/share/share.php4
-rw-r--r--tests/ocs/response.php42
-rw-r--r--tests/settings/controller/CertificateControllerTest.php25
-rw-r--r--tests/settings/controller/CheckSetupControllerTest.php45
-rw-r--r--themes/example/defaults.php6
135 files changed, 5454 insertions, 557 deletions
diff --git a/3rdparty b/3rdparty
-Subproject 0590498b38aa0c760e2ad7af4fbd19787d62ed4
+Subproject b94f7d38f6e13825fd34c7113827d3c369a689a
diff --git a/apps/encryption/js/encryption.js b/apps/encryption/js/encryption.js
index a6c1bea89b2..361347b44b7 100644
--- a/apps/encryption/js/encryption.js
+++ b/apps/encryption/js/encryption.js
@@ -5,15 +5,11 @@
* See the COPYING-README file.
*/
-if (!OC.Encryption) {
- OC.Encryption = {};
-}
-
/**
* @namespace
* @memberOf OC
*/
-OC.Encryption = {
+OC.Encryption = _.extend(OC.Encryption || {}, {
displayEncryptionWarning: function () {
if (!OC.currentUser || !OC.Notification.isHidden()) {
return;
@@ -28,7 +24,7 @@ OC.Encryption = {
}
);
}
-};
+});
$(document).ready(function() {
// wait for other apps/extensions to register their event handlers and file actions
// in the "ready" clause
diff --git a/apps/encryption/js/settings-personal.js b/apps/encryption/js/settings-personal.js
index fa94bea6bc5..75ebab5059c 100644
--- a/apps/encryption/js/settings-personal.js
+++ b/apps/encryption/js/settings-personal.js
@@ -4,11 +4,7 @@
* See the COPYING-README file.
*/
-if (!OC.Encryption) {
- OC.Encryption = {};
-}
-
-OC.Encryption = {
+OC.Encryption = _.extend(OC.Encryption || {}, {
updatePrivateKeyPassword: function () {
var oldPrivateKeyPassword = $('input:password[id="oldPrivateKeyPassword"]').val();
var newPrivateKeyPassword = $('input:password[id="newPrivateKeyPassword"]').val();
@@ -26,7 +22,7 @@ OC.Encryption = {
OC.msg.finishedError('#ocDefaultEncryptionModule .msg', JSON.parse(jqXHR.responseText).message);
});
}
-};
+});
$(document).ready(function () {
diff --git a/apps/encryption/lib/migration.php b/apps/encryption/lib/migration.php
index d22c571fd40..5396a7db627 100644
--- a/apps/encryption/lib/migration.php
+++ b/apps/encryption/lib/migration.php
@@ -72,7 +72,7 @@ class Migration {
// only update during the first run
if ($this->installedVersion !== '-1') {
$query = $this->connection->getQueryBuilder();
- $query->update('*PREFIX*filecache')
+ $query->update('filecache')
->set('size', 'unencrypted_size')
->where($query->expr()->eq('encrypted', $query->createParameter('encrypted')))
->setParameter('encrypted', 1);
@@ -163,7 +163,7 @@ class Migration {
$oldAppValues = $this->connection->getQueryBuilder();
$oldAppValues->select('*')
- ->from('*PREFIX*appconfig')
+ ->from('appconfig')
->where($oldAppValues->expr()->eq('appid', $oldAppValues->createParameter('appid')))
->setParameter('appid', 'files_encryption');
$appSettings = $oldAppValues->execute();
@@ -178,7 +178,7 @@ class Migration {
$oldPreferences = $this->connection->getQueryBuilder();
$oldPreferences->select('*')
- ->from('*PREFIX*preferences')
+ ->from('preferences')
->where($oldPreferences->expr()->eq('appid', $oldPreferences->createParameter('appid')))
->setParameter('appid', 'files_encryption');
$preferenceSettings = $oldPreferences->execute();
diff --git a/apps/encryption/tests/lib/MigrationTest.php b/apps/encryption/tests/lib/MigrationTest.php
index 5bc3b89b5b9..bb1f0a310a2 100644
--- a/apps/encryption/tests/lib/MigrationTest.php
+++ b/apps/encryption/tests/lib/MigrationTest.php
@@ -291,12 +291,12 @@ class MigrationTest extends \Test\TestCase {
/** @var \OCP\IDBConnection $connection */
$connection = \OC::$server->getDatabaseConnection();
$query = $connection->getQueryBuilder();
- $query->delete('*PREFIX*appconfig')
+ $query->delete('appconfig')
->where($query->expr()->eq('appid', $query->createParameter('appid')))
->setParameter('appid', 'encryption');
$query->execute();
$query = $connection->getQueryBuilder();
- $query->delete('*PREFIX*preferences')
+ $query->delete('preferences')
->where($query->expr()->eq('appid', $query->createParameter('appid')))
->setParameter('appid', 'encryption');
$query->execute();
@@ -309,10 +309,10 @@ class MigrationTest extends \Test\TestCase {
$this->invokePrivate($m, 'installedVersion', ['0.7']);
$m->updateDB();
- $this->verifyDB('*PREFIX*appconfig', 'files_encryption', 0);
- $this->verifyDB('*PREFIX*preferences', 'files_encryption', 0);
- $this->verifyDB('*PREFIX*appconfig', 'encryption', 3);
- $this->verifyDB('*PREFIX*preferences', 'encryption', 1);
+ $this->verifyDB('appconfig', 'files_encryption', 0);
+ $this->verifyDB('preferences', 'files_encryption', 0);
+ $this->verifyDB('appconfig', 'encryption', 3);
+ $this->verifyDB('preferences', 'encryption', 1);
}
@@ -329,17 +329,17 @@ class MigrationTest extends \Test\TestCase {
$this->invokePrivate($m, 'installedVersion', ['0.7']);
$m->updateDB();
- $this->verifyDB('*PREFIX*appconfig', 'files_encryption', 0);
- $this->verifyDB('*PREFIX*preferences', 'files_encryption', 0);
- $this->verifyDB('*PREFIX*appconfig', 'encryption', 3);
- $this->verifyDB('*PREFIX*preferences', 'encryption', 1);
+ $this->verifyDB('appconfig', 'files_encryption', 0);
+ $this->verifyDB('preferences', 'files_encryption', 0);
+ $this->verifyDB('appconfig', 'encryption', 3);
+ $this->verifyDB('preferences', 'encryption', 1);
// check if the existing values where overwritten correctly
/** @var \OC\DB\Connection $connection */
$connection = \OC::$server->getDatabaseConnection();
$query = $connection->getQueryBuilder();
$query->select('configvalue')
- ->from('*PREFIX*appconfig')
+ ->from('appconfig')
->where($query->expr()->andX(
$query->expr()->eq('appid', $query->createParameter('appid')),
$query->expr()->eq('configkey', $query->createParameter('configkey'))
@@ -353,7 +353,7 @@ class MigrationTest extends \Test\TestCase {
$query = $connection->getQueryBuilder();
$query->select('configvalue')
- ->from('*PREFIX*preferences')
+ ->from('preferences')
->where($query->expr()->andX(
$query->expr()->eq('appid', $query->createParameter('appid')),
$query->expr()->eq('configkey', $query->createParameter('configkey')),
@@ -399,7 +399,7 @@ class MigrationTest extends \Test\TestCase {
$connection = \OC::$server->getDatabaseConnection();
$query = $connection->getQueryBuilder();
$query->select('*')
- ->from('*PREFIX*filecache');
+ ->from('filecache');
$result = $query->execute();
$entries = $result->fetchAll();
foreach($entries as $entry) {
@@ -417,15 +417,15 @@ class MigrationTest extends \Test\TestCase {
/** @var \OCP\IDBConnection $connection */
$connection = \OC::$server->getDatabaseConnection();
$query = $connection->getQueryBuilder();
- $query->delete('*PREFIX*filecache');
+ $query->delete('filecache');
$query->execute();
$query = $connection->getQueryBuilder();
$result = $query->select('fileid')
- ->from('*PREFIX*filecache')
+ ->from('filecache')
->setMaxResults(1)->execute()->fetchAll();
$this->assertEmpty($result);
$query = $connection->getQueryBuilder();
- $query->insert('*PREFIX*filecache')
+ $query->insert('filecache')
->values(
array(
'storage' => $query->createParameter('storage'),
@@ -447,7 +447,7 @@ class MigrationTest extends \Test\TestCase {
}
$query = $connection->getQueryBuilder();
$result = $query->select('fileid')
- ->from('*PREFIX*filecache')
+ ->from('filecache')
->execute()->fetchAll();
$this->assertSame(19, count($result));
}
diff --git a/apps/files/appinfo/application.php b/apps/files/appinfo/application.php
index c8aaf375d96..6ba77e09556 100644
--- a/apps/files/appinfo/application.php
+++ b/apps/files/appinfo/application.php
@@ -1,6 +1,5 @@
<?php
/**
- * @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
* @author Vincent Petry <pvince81@owncloud.com>
@@ -21,8 +20,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
-
-namespace OCA\Files\Appinfo;
+namespace OCA\Files\AppInfo;
use OCA\Files\Controller\ApiController;
use OCP\AppFramework\App;
@@ -68,5 +66,10 @@ class Application extends App {
$homeFolder
);
});
+
+ /*
+ * Register capabilities
+ */
+ $container->registerCapability('OCA\Files\Capabilities');
}
}
diff --git a/apps/files/appinfo/routes.php b/apps/files/appinfo/routes.php
index 5aa52f17a29..d1b8954d5ce 100644
--- a/apps/files/appinfo/routes.php
+++ b/apps/files/appinfo/routes.php
@@ -1,9 +1,9 @@
<?php
/**
* @author Bart Visscher <bartv@thisnet.nl>
- * @author Joas Schilling <nickvergessen@owncloud.com>
* @author Lukas Reschke <lukas@owncloud.com>
* @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tobias Kaminsky <tobias@kaminsky.me>
* @author Tom Needham <tom@owncloud.com>
* @author Vincent Petry <pvince81@owncloud.com>
@@ -24,8 +24,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
-
-namespace OCA\Files\Appinfo;
+namespace OCA\Files\AppInfo;
$application = new Application();
$application->registerRoutes(
@@ -82,6 +81,4 @@ $this->create('files_ajax_upload', 'ajax/upload.php')
$this->create('download', 'download{file}')
->requirements(array('file' => '.*'))
->actionInclude('files/download.php');
-
-// Register with the capabilities API
-\OCP\API::register('get', '/cloud/capabilities', array('OCA\Files\Capabilities', 'getCapabilities'), 'files', \OCP\API::USER_AUTH);
+
diff --git a/apps/files/css/detailsView.css b/apps/files/css/detailsView.css
new file mode 100644
index 00000000000..76629cb790f
--- /dev/null
+++ b/apps/files/css/detailsView.css
@@ -0,0 +1,55 @@
+#app-sidebar .detailFileInfoContainer {
+ min-height: 50px;
+ padding: 15px;
+}
+
+#app-sidebar .detailFileInfoContainer > div {
+ clear: both;
+}
+
+#app-sidebar .mainFileInfoView {
+ margin-right: 20px; /* accomodate for close icon */
+}
+
+#app-sidebar .thumbnail {
+ width: 50px;
+ height: 50px;
+ float: left;
+ margin-right: 10px;
+ background-size: 50px;
+}
+
+#app-sidebar .ellipsis {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+#app-sidebar .fileName {
+ font-size: 16px;
+ padding-top: 3px;
+}
+
+#app-sidebar .file-details {
+ margin-top: 3px;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+ opacity: .5;
+}
+#app-sidebar .action-favorite {
+ vertical-align: text-bottom;
+ padding: 10px;
+ margin: -10px;
+}
+
+#app-sidebar .detailList {
+ float: left;
+}
+
+#app-sidebar .close {
+ position: absolute;
+ top: 0;
+ right: 0;
+ padding: 15px;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+ opacity: .5;
+}
diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index f2f2c5ac3bc..7e3318a962b 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -103,6 +103,10 @@
min-height: 100%;
}
+.app-files #app-content {
+ overflow-x: hidden;
+}
+
/* icons for sidebar */
.nav-icon-files {
background-image: url('../img/folder.svg');
@@ -143,6 +147,7 @@
#filestable tbody tr:active {
background-color: rgb(240,240,240);
}
+#filestable tbody tr.highlighted,
#filestable tbody tr.selected {
background-color: rgb(230,230,230);
}
diff --git a/apps/files/index.php b/apps/files/index.php
index 4f103f975cb..dca3e5ae74d 100644
--- a/apps/files/index.php
+++ b/apps/files/index.php
@@ -50,6 +50,12 @@ OCP\Util::addscript('files', 'search');
\OCP\Util::addScript('files', 'tagsplugin');
\OCP\Util::addScript('files', 'favoritesplugin');
+\OCP\Util::addScript('files', 'detailfileinfoview');
+\OCP\Util::addScript('files', 'detailtabview');
+\OCP\Util::addScript('files', 'mainfileinfodetailview');
+\OCP\Util::addScript('files', 'detailsview');
+\OCP\Util::addStyle('files', 'detailsView');
+
\OC_Util::addVendorScript('core', 'handlebars/handlebars');
OCP\App::setActiveNavigationEntry('files_index');
diff --git a/apps/files/js/detailfileinfoview.js b/apps/files/js/detailfileinfoview.js
new file mode 100644
index 00000000000..9a88b5e2d8a
--- /dev/null
+++ b/apps/files/js/detailfileinfoview.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ /**
+ * @class OCA.Files.DetailFileInfoView
+ * @classdesc
+ *
+ * Displays a block of details about the file info.
+ *
+ */
+ var DetailFileInfoView = function() {
+ this.initialize();
+ };
+ /**
+ * @memberof OCA.Files
+ */
+ DetailFileInfoView.prototype = {
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ _template: null,
+
+ /**
+ * Currently displayed file info
+ *
+ * @type OCA.Files.FileInfo
+ */
+ _fileInfo: null,
+
+ /**
+ * Initialize the details view
+ */
+ initialize: function() {
+ this.$el = $('<div class="detailFileInfoView"></div>');
+ },
+
+ /**
+ * returns the jQuery object for HTML output
+ *
+ * @returns {jQuery}
+ */
+ get$: function() {
+ return this.$el;
+ },
+
+ /**
+ * Destroy / uninitialize this instance.
+ */
+ destroy: function() {
+ if (this.$el) {
+ this.$el.remove();
+ }
+ },
+
+ /**
+ * Renders this details view
+ *
+ * @abstract
+ */
+ render: function() {
+ // to be implemented in subclass
+ },
+
+ /**
+ * Sets the file info to be displayed in the view
+ *
+ * @param {OCA.Files.FileInfo} fileInfo file info to set
+ */
+ setFileInfo: function(fileInfo) {
+ this._fileInfo = fileInfo;
+ this.render();
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ }
+ };
+
+ OCA.Files.DetailFileInfoView = DetailFileInfoView;
+})();
+
diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js
new file mode 100644
index 00000000000..7b7bd013f9e
--- /dev/null
+++ b/apps/files/js/detailsview.js
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+ var TEMPLATE =
+ '<div>' +
+ ' <div class="detailFileInfoContainer">' +
+ ' </div>' +
+ ' <div>' +
+ ' <ul class="tabHeaders">' +
+ ' </ul>' +
+ ' <div class="tabsContainer">' +
+ ' </div>' +
+ ' </div>' +
+ ' <a class="close icon-close" href="#" alt="{{closeLabel}}"></a>' +
+ '</div>';
+
+ var TEMPLATE_TAB_HEADER =
+ '<li class="tabHeader {{#if selected}}selected{{/if}}" data-tabid="{{tabId}}" data-tabindex="{{tabIndex}}"><a href="#">{{label}}</a></li>';
+
+ /**
+ * @class OCA.Files.DetailsView
+ * @classdesc
+ *
+ * The details view show details about a selected file.
+ *
+ */
+ var DetailsView = function() {
+ this.initialize();
+ };
+
+ /**
+ * @memberof OCA.Files
+ */
+ DetailsView.prototype = {
+
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ _template: null,
+ _templateTabHeader: null,
+
+ /**
+ * Currently displayed file info
+ *
+ * @type OCA.Files.FileInfo
+ */
+ _fileInfo: null,
+
+ /**
+ * List of detail tab views
+ *
+ * @type Array<OCA.Files.DetailTabView>
+ */
+ _tabViews: [],
+
+ /**
+ * List of detail file info views
+ *
+ * @type Array<OCA.Files.DetailFileInfoView>
+ */
+ _detailFileInfoViews: [],
+
+ /**
+ * Id of the currently selected tab
+ *
+ * @type string
+ */
+ _currentTabId: null,
+
+ /**
+ * Initialize the details view
+ */
+ initialize: function() {
+ this.$el = $('<div id="app-sidebar"></div>');
+ this.fileInfo = null;
+ this._tabViews = [];
+ this._detailFileInfoViews = [];
+
+ this.$el.on('click', 'a.close', function(event) {
+ OC.Apps.hideAppSidebar();
+ event.preventDefault();
+ });
+
+ this.$el.on('click', '.tabHeaders .tabHeader', _.bind(this._onClickTab, this));
+
+ // uncomment to add some dummy tabs for testing
+ //this._addTestTabs();
+ },
+
+ /**
+ * Destroy / uninitialize this instance.
+ */
+ destroy: function() {
+ if (this.$el) {
+ this.$el.remove();
+ }
+ },
+
+ _onClickTab: function(e) {
+ var $target = $(e.target);
+ if (!$target.hasClass('tabHeader')) {
+ $target = $target.closest('.tabHeader');
+ }
+ var tabIndex = $target.attr('data-tabindex');
+ var targetTab;
+ if (_.isUndefined(tabIndex)) {
+ return;
+ }
+
+ this.$el.find('.tabsContainer .tab').addClass('hidden');
+ targetTab = this._tabViews[tabIndex];
+ targetTab.$el.removeClass('hidden');
+
+ this.$el.find('.tabHeaders li').removeClass('selected');
+ $target.addClass('selected');
+
+ e.preventDefault();
+ },
+
+ _addTestTabs: function() {
+ for (var j = 0; j < 2; j++) {
+ var testView = new OCA.Files.DetailTabView('testtab' + j);
+ testView.index = j;
+ testView.getLabel = function() { return 'Test tab ' + this.index; };
+ testView.render = function() {
+ this.$el.empty();
+ for (var i = 0; i < 100; i++) {
+ this.$el.append('<div>Test tab ' + this.index + ' row ' + i + '</div>');
+ }
+ };
+ this._tabViews.push(testView);
+ }
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ var self = this;
+ this.$el.empty();
+
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ if (!this._templateTabHeader) {
+ this._templateTabHeader = Handlebars.compile(TEMPLATE_TAB_HEADER);
+ }
+
+ var $el = $(this._template({
+ closeLabel: t('files', 'Close')
+ }));
+ var $tabsContainer = $el.find('.tabsContainer');
+ var $tabHeadsContainer = $el.find('.tabHeaders');
+ var $detailsContainer = $el.find('.detailFileInfoContainer');
+
+ // render details
+ _.each(this._detailFileInfoViews, function(detailView) {
+ $detailsContainer.append(detailView.get$());
+ });
+
+ if (this._tabViews.length > 0) {
+ if (!this._currentTab) {
+ this._currentTab = this._tabViews[0].getId();
+ }
+
+ // render tabs
+ _.each(this._tabViews, function(tabView, i) {
+ // hidden by default
+ var $el = tabView.get$();
+ var isCurrent = (tabView.getId() === self._currentTab);
+ if (!isCurrent) {
+ $el.addClass('hidden');
+ }
+ $tabsContainer.append($el);
+
+ $tabHeadsContainer.append(self._templateTabHeader({
+ tabId: tabView.getId(),
+ tabIndex: i,
+ label: tabView.getLabel(),
+ selected: isCurrent
+ }));
+ });
+ }
+
+ // TODO: select current tab
+
+ this.$el.append($el);
+ },
+
+ /**
+ * Sets the file info to be displayed in the view
+ *
+ * @param {OCA.Files.FileInfo} fileInfo file info to set
+ */
+ setFileInfo: function(fileInfo) {
+ this._fileInfo = fileInfo;
+
+ this.render();
+
+ // notify all panels
+ _.each(this._tabViews, function(tabView) {
+ tabView.setFileInfo(fileInfo);
+ });
+ _.each(this._detailFileInfoViews, function(detailView) {
+ detailView.setFileInfo(fileInfo);
+ });
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ },
+
+ /**
+ * Adds a tab in the tab view
+ *
+ * @param {OCA.Files.DetailTabView} tab view
+ */
+ addTabView: function(tabView) {
+ this._tabViews.push(tabView);
+ },
+
+ /**
+ * Adds a detail view for file info.
+ *
+ * @param {OCA.Files.DetailFileInfoView} detail view
+ */
+ addDetailView: function(detailView) {
+ this._detailFileInfoViews.push(detailView);
+ }
+ };
+
+ OCA.Files.DetailsView = DetailsView;
+})();
+
diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js
new file mode 100644
index 00000000000..b9b1dda2ccc
--- /dev/null
+++ b/apps/files/js/detailtabview.js
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+
+ /**
+ * @class OCA.Files.DetailTabView
+ * @classdesc
+ *
+ * Base class for tab views to display file information.
+ *
+ */
+ var DetailTabView = function(id) {
+ this.initialize(id);
+ };
+
+ /**
+ * @memberof OCA.Files
+ */
+ DetailTabView.prototype = {
+ /**
+ * jQuery element
+ */
+ $el: null,
+
+ /**
+ * Tab id
+ */
+ _id: null,
+
+ /**
+ * Tab label
+ */
+ _label: null,
+
+ _template: null,
+
+ /**
+ * Currently displayed file info
+ *
+ * @type OCA.Files.FileInfo
+ */
+ _fileInfo: null,
+
+ /**
+ * Initialize the details view
+ *
+ * @param {string} id tab id
+ */
+ initialize: function(id) {
+ if (!id) {
+ throw 'Argument "id" is required';
+ }
+ this._id = id;
+ this.$el = $('<div class="tab"></div>');
+ this.$el.attr('data-tabid', id);
+ },
+
+ /**
+ * Destroy / uninitialize this instance.
+ */
+ destroy: function() {
+ if (this.$el) {
+ this.$el.remove();
+ }
+ },
+
+ /**
+ * Returns the tab element id
+ *
+ * @return {string} tab id
+ */
+ getId: function() {
+ return this._id;
+ },
+
+ /**
+ * Returns the tab label
+ *
+ * @return {String} label
+ */
+ getLabel: function() {
+ return 'Tab ' + this._id;
+ },
+
+ /**
+ * returns the jQuery object for HTML output
+ *
+ * @returns {jQuery}
+ */
+ get$: function() {
+ return this.$el;
+ },
+
+ /**
+ * Renders this details view
+ *
+ * @abstract
+ */
+ render: function() {
+ // to be implemented in subclass
+ // FIXME: code is only for testing
+ this.$el.empty();
+ this.$el.append('<div>Hello ' + this._id + '</div>');
+ },
+
+ /**
+ * Sets the file info to be displayed in the view
+ *
+ * @param {OCA.Files.FileInfo} fileInfo file info to set
+ */
+ setFileInfo: function(fileInfo) {
+ this._fileInfo = fileInfo;
+ this.render();
+ },
+
+ /**
+ * Returns the file info.
+ *
+ * @return {OCA.Files.FileInfo} file info
+ */
+ getFileInfo: function() {
+ return this._fileInfo;
+ }
+ };
+
+ OCA.Files.DetailTabView = DetailTabView;
+})();
+
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index a7d4e41d0e0..f5629ecd2c3 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -23,6 +23,7 @@
* @param [options.scrollContainer] scrollable container, defaults to $(window)
* @param [options.dragOptions] drag options, disabled by default
* @param [options.folderDropOptions] folder drop options, disabled by default
+ * @param [options.detailsViewEnabled=true] whether to enable details view
*/
var FileList = function($el, options) {
this.initialize($el, options);
@@ -65,6 +66,11 @@
fileSummary: null,
/**
+ * @type OCA.Files.DetailsView
+ */
+ _detailsView: null,
+
+ /**
* Whether the file list was initialized already.
* @type boolean
*/
@@ -205,6 +211,13 @@
}
this.breadcrumb = new OCA.Files.BreadCrumb(breadcrumbOptions);
+ if (_.isUndefined(options.detailsViewEnabled) || options.detailsViewEnabled) {
+ this._detailsView = new OCA.Files.DetailsView();
+ this._detailsView.addDetailView(new OCA.Files.MainFileInfoDetailView());
+ this._detailsView.$el.insertBefore(this.$el);
+ this._detailsView.$el.addClass('disappear');
+ }
+
this.$el.find('#controls').prepend(this.breadcrumb.$el);
this.$el.find('thead th .columntitle').click(_.bind(this._onClickHeader, this));
@@ -216,6 +229,13 @@
this.updateSearch();
+ this.$el.on('click', function(event) {
+ var $target = $(event.target);
+ // click outside file row ?
+ if (!$target.closest('tbody').length && !$target.closest('#app-sidebar').length) {
+ self._updateDetailsView(null);
+ }
+ });
this.$fileList.on('click','td.filename>a.name', _.bind(this._onClickFile, this));
this.$fileList.on('change', 'td.filename>.selectCheckBox', _.bind(this._onClickFileCheckbox, this));
this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
@@ -263,6 +283,37 @@
},
/**
+ * Update the details view to display the given file
+ *
+ * @param {OCA.Files.FileInfo} fileInfo file info to display
+ */
+ _updateDetailsView: function(fileInfo) {
+ if (!this._detailsView) {
+ return;
+ }
+
+ var self = this;
+ var oldFileInfo = this._detailsView.getFileInfo();
+ if (oldFileInfo) {
+ // TODO: use more efficient way, maybe track the highlight
+ this.$fileList.children().filterAttr('data-id', '' + oldFileInfo.id).removeClass('highlighted');
+ }
+
+ if (!fileInfo) {
+ OC.Apps.hideAppSidebar();
+ this._detailsView.setFileInfo(null);
+ return;
+ }
+
+ this.$fileList.children().filterAttr('data-id', '' + fileInfo.id).addClass('highlighted');
+ this._detailsView.setFileInfo(_.extend({
+ path: this.getCurrentDirectory()
+ }, fileInfo));
+ this._detailsView.$el.scrollTop(0);
+ _.defer(OC.Apps.showAppSidebar);
+ },
+
+ /**
* Event handler for when the window size changed
*/
_onResize: function() {
@@ -315,6 +366,12 @@
delete this._selectedFiles[$tr.data('id')];
this._selectionSummary.remove(data);
}
+ if (this._selectionSummary.getTotal() === 1) {
+ this._updateDetailsView(_.values(this._selectedFiles)[0]);
+ } else {
+ // show nothing when multiple files are selected
+ this._updateDetailsView(null);
+ }
this.$el.find('.select-all').prop('checked', this._selectionSummary.getTotal() === this.files.length);
},
@@ -350,27 +407,34 @@
this._selectFileEl($tr, !$checkbox.prop('checked'));
this.updateSelectionSummary();
} else {
- var filename = $tr.attr('data-file');
- var renaming = $tr.data('renaming');
- if (!renaming) {
- this.fileActions.currentFile = $tr.find('td');
- var mime = this.fileActions.getCurrentMimeType();
- var type = this.fileActions.getCurrentType();
- var permissions = this.fileActions.getCurrentPermissions();
- var action = this.fileActions.getDefault(mime,type, permissions);
- if (action) {
- event.preventDefault();
- // also set on global object for legacy apps
- window.FileActions.currentFile = this.fileActions.currentFile;
- action(filename, {
- $file: $tr,
- fileList: this,
- fileActions: this.fileActions,
- dir: $tr.attr('data-path') || this.getCurrentDirectory()
- });
+ // clicked directly on the name
+ if (!this._detailsView || $(event.target).is('.nametext') || $(event.target).closest('.nametext').length) {
+ var filename = $tr.attr('data-file');
+ var renaming = $tr.data('renaming');
+ if (!renaming) {
+ this.fileActions.currentFile = $tr.find('td');
+ var mime = this.fileActions.getCurrentMimeType();
+ var type = this.fileActions.getCurrentType();
+ var permissions = this.fileActions.getCurrentPermissions();
+ var action = this.fileActions.getDefault(mime,type, permissions);
+ if (action) {
+ event.preventDefault();
+ // also set on global object for legacy apps
+ window.FileActions.currentFile = this.fileActions.currentFile;
+ action(filename, {
+ $file: $tr,
+ fileList: this,
+ fileActions: this.fileActions,
+ dir: $tr.attr('data-path') || this.getCurrentDirectory()
+ });
+ }
+ // deselect row
+ $(event.target).closest('a').blur();
}
- // deselect row
- $(event.target).closest('a').blur();
+ } else {
+ var fileInfo = this.files[$tr.index()];
+ this._updateDetailsView(fileInfo);
+ event.preventDefault();
}
}
},
@@ -825,7 +889,7 @@
var formatted;
var text;
if (mtime > 0) {
- formatted = formatDate(mtime);
+ formatted = OC.Util.formatDate(mtime);
text = OC.Util.relativeModifiedDate(mtime);
} else {
formatted = t('files', 'Unable to determine date');
@@ -1239,6 +1303,12 @@
ready(iconURL); // set mimeicon URL
urlSpec.file = OCA.Files.Files.fixPath(path);
+ if (options.x) {
+ urlSpec.x = options.x;
+ }
+ if (options.y) {
+ urlSpec.y = options.y;
+ }
if (etag){
// use etag as cache buster
@@ -1521,6 +1591,7 @@
tr.remove();
tr = self.add(fileInfo, {updateSummary: false, silent: true});
self.$fileList.trigger($.Event('fileActionsReady', {fileList: self, $files: $(tr)}));
+ self._updateDetailsView(fileInfo);
}
});
} else {
@@ -1677,6 +1748,7 @@
}
this.$table.addClass('hidden');
+ this.$el.find('#emptycontent').addClass('hidden');
$mask = $('<div class="mask transparent"></div>');
@@ -2176,6 +2248,20 @@
}
});
+ },
+
+ /**
+ * Register a tab view to be added to all views
+ */
+ registerTabView: function(tabView) {
+ this._detailsView.addTabView(tabView);
+ },
+
+ /**
+ * Register a detail view to be added to all views
+ */
+ registerDetailView: function(detailView) {
+ this._detailsView.addDetailView(detailView);
}
};
diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js
new file mode 100644
index 00000000000..a00d907d0d6
--- /dev/null
+++ b/apps/files/js/mainfileinfodetailview.js
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ var TEMPLATE =
+ '<div class="thumbnail"></div><div title="{{name}}" class="fileName ellipsis">{{name}}</div>' +
+ '<div class="file-details ellipsis">' +
+ ' <a href="#" ' +
+ ' alt="{{starAltText}}"' +
+ ' class="action action-favorite favorite">' +
+ ' <img class="svg" src="{{starIcon}}" />' +
+ ' </a>' +
+ ' <span class="size" title="{{altSize}}">{{size}}</span>, <span class="date" title="{{altDate}}">{{date}}</span>' +
+ '</div>';
+
+ /**
+ * @class OCA.Files.MainFileInfoDetailView
+ * @classdesc
+ *
+ * Displays main details about a file
+ *
+ */
+ var MainFileInfoDetailView = function() {
+ this.initialize();
+ };
+ /**
+ * @memberof OCA.Files
+ */
+ MainFileInfoDetailView.prototype = _.extend({}, OCA.Files.DetailFileInfoView.prototype,
+ /** @lends OCA.Files.MainFileInfoDetailView.prototype */ {
+ _template: null,
+
+ /**
+ * Initialize the details view
+ */
+ initialize: function() {
+ this.$el = $('<div class="mainFileInfoView"></div>');
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ this.$el.empty();
+
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ if (this._fileInfo) {
+ var isFavorite = (this._fileInfo.tags || []).indexOf(OC.TAG_FAVORITE) >= 0;
+ this.$el.append(this._template({
+ nameLabel: t('files', 'Name'),
+ name: this._fileInfo.name,
+ pathLabel: t('files', 'Path'),
+ path: this._fileInfo.path,
+ sizeLabel: t('files', 'Size'),
+ size: OC.Util.humanFileSize(this._fileInfo.size, true),
+ altSize: n('files', '%n byte', '%n bytes', this._fileInfo.size),
+ dateLabel: t('files', 'Modified'),
+ altDate: OC.Util.formatDate(this._fileInfo.mtime),
+ date: OC.Util.relativeModifiedDate(this._fileInfo.mtime),
+ starAltText: isFavorite ? t('files', 'Favorited') : t('files', 'Favorite'),
+ starIcon: OC.imagePath('core', isFavorite ? 'actions/starred' : 'actions/star')
+ }));
+
+ // TODO: we really need OC.Previews
+ var $iconDiv = this.$el.find('.thumbnail');
+ if (this._fileInfo.mimetype !== 'httpd/unix-directory') {
+ // TODO: inject utility class?
+ FileList.lazyLoadPreview({
+ path: this._fileInfo.path + '/' + this._fileInfo.name,
+ mime: this._fileInfo.mimetype,
+ etag: this._fileInfo.etag,
+ x: 50,
+ y: 50,
+ callback: function(previewUrl) {
+ $iconDiv.css('background-image', 'url("' + previewUrl + '")');
+ }
+ });
+ } else {
+ // TODO: special icons / shared / external
+ $iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")');
+ }
+ this.$el.find('[title]').tooltip({placement: 'bottom'});
+ }
+ }
+ });
+
+ OCA.Files.MainFileInfoDetailView = MainFileInfoDetailView;
+})();
diff --git a/apps/files/lib/capabilities.php b/apps/files/lib/capabilities.php
index 05d12864dca..2e19283e4d6 100644
--- a/apps/files/lib/capabilities.php
+++ b/apps/files/lib/capabilities.php
@@ -1,7 +1,7 @@
<?php
/**
* @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tom Needham <tom@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
@@ -20,19 +20,28 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
-
-namespace OCA\Files;
-class Capabilities {
-
- public static function getCapabilities() {
- return new \OC_OCS_Result(array(
- 'capabilities' => array(
- 'files' => array(
- 'bigfilechunking' => true,
- ),
- ),
- ));
+namespace OCA\Files;
+
+use OCP\Capabilities\ICapability;
+
+/**
+ * Class Capabilities
+ *
+ * @package OCA\Files
+ */
+class Capabilities implements ICapability {
+
+ /**
+ * Return this classes capabilities
+ *
+ * @return array
+ */
+ public function getCapabilities() {
+ return [
+ 'files' => [
+ 'bigfilechunking' => true,
+ ],
+ ];
}
-
}
diff --git a/apps/files/tests/js/detailsviewSpec.js b/apps/files/tests/js/detailsviewSpec.js
new file mode 100644
index 00000000000..db1e24fd68e
--- /dev/null
+++ b/apps/files/tests/js/detailsviewSpec.js
@@ -0,0 +1,105 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+describe('OCA.Files.DetailsView tests', function() {
+ var detailsView;
+
+ beforeEach(function() {
+ detailsView = new OCA.Files.DetailsView();
+ });
+ afterEach(function() {
+ detailsView.destroy();
+ detailsView = undefined;
+ });
+ it('renders itself empty when nothing registered', function() {
+ detailsView.render();
+ expect(detailsView.$el.find('.detailFileInfoContainer').length).toEqual(1);
+ expect(detailsView.$el.find('.tabsContainer').length).toEqual(1);
+ });
+ describe('file info detail view', function() {
+ it('renders registered view', function() {
+ var testView = new OCA.Files.DetailFileInfoView();
+ var testView2 = new OCA.Files.DetailFileInfoView();
+ detailsView.addDetailView(testView);
+ detailsView.addDetailView(testView2);
+ detailsView.render();
+
+ expect(detailsView.$el.find('.detailFileInfoContainer .detailFileInfoView').length).toEqual(2);
+ });
+ it('updates registered tabs when fileinfo is updated', function() {
+ var viewRenderStub = sinon.stub(OCA.Files.DetailFileInfoView.prototype, 'render');
+ var testView = new OCA.Files.DetailFileInfoView();
+ var testView2 = new OCA.Files.DetailFileInfoView();
+ detailsView.addDetailView(testView);
+ detailsView.addDetailView(testView2);
+ detailsView.render();
+
+ var fileInfo = {id: 5, name: 'test.txt'};
+ viewRenderStub.reset();
+ detailsView.setFileInfo(fileInfo);
+
+ expect(testView.getFileInfo()).toEqual(fileInfo);
+ expect(testView2.getFileInfo()).toEqual(fileInfo);
+
+ expect(viewRenderStub.callCount).toEqual(2);
+ viewRenderStub.restore();
+ });
+ });
+ describe('tabs', function() {
+ var testView, testView2;
+
+ beforeEach(function() {
+ testView = new OCA.Files.DetailTabView('test1');
+ testView2 = new OCA.Files.DetailTabView('test2');
+ detailsView.addTabView(testView);
+ detailsView.addTabView(testView2);
+ detailsView.render();
+ });
+ it('renders registered tabs', function() {
+ expect(detailsView.$el.find('.tab').length).toEqual(2);
+ });
+ it('updates registered tabs when fileinfo is updated', function() {
+ var tabRenderStub = sinon.stub(OCA.Files.DetailTabView.prototype, 'render');
+ var fileInfo = {id: 5, name: 'test.txt'};
+ tabRenderStub.reset();
+ detailsView.setFileInfo(fileInfo);
+
+ expect(testView.getFileInfo()).toEqual(fileInfo);
+ expect(testView2.getFileInfo()).toEqual(fileInfo);
+
+ expect(tabRenderStub.callCount).toEqual(2);
+ tabRenderStub.restore();
+ });
+ it('selects the first tab by default', function() {
+ expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(true);
+ expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(false);
+ expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(false);
+ expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(true);
+ });
+ it('switches the current tab when clicking on tab header', function() {
+ detailsView.$el.find('.tabHeader').eq(1).click();
+ expect(detailsView.$el.find('.tabHeader').eq(0).hasClass('selected')).toEqual(false);
+ expect(detailsView.$el.find('.tabHeader').eq(1).hasClass('selected')).toEqual(true);
+ expect(detailsView.$el.find('.tab').eq(0).hasClass('hidden')).toEqual(true);
+ expect(detailsView.$el.find('.tab').eq(1).hasClass('hidden')).toEqual(false);
+ });
+ });
+});
diff --git a/apps/files/tests/js/favoritespluginspec.js b/apps/files/tests/js/favoritespluginspec.js
index 90b40ede74b..1b144c28707 100644
--- a/apps/files/tests/js/favoritespluginspec.js
+++ b/apps/files/tests/js/favoritespluginspec.js
@@ -113,7 +113,7 @@ describe('OCA.Files.FavoritesPlugin tests', function() {
shareOwner: 'user2'
}]);
- fileList.findFileEl('testdir').find('td a.name').click();
+ fileList.findFileEl('testdir').find('td .nametext').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js
index 09d698088ae..5c0c8c96bc5 100644
--- a/apps/files/tests/js/filelistSpec.js
+++ b/apps/files/tests/js/filelistSpec.js
@@ -1870,6 +1870,50 @@ describe('OCA.Files.FileList tests', function() {
});
})
});
+ describe('Details sidebar', function() {
+ beforeEach(function() {
+ fileList.setFiles(testFiles);
+ });
+ it('Clicking on a file row will trigger file action if no details view configured', function() {
+ fileList._detailsView = null;
+ var updateDetailsViewStub = sinon.stub(fileList, '_updateDetailsView');
+ var actionStub = sinon.stub();
+ fileList.setFiles(testFiles);
+ fileList.fileActions.register(
+ 'text/plain',
+ 'Test',
+ OC.PERMISSION_ALL,
+ function() {
+ // Specify icon for hitory button
+ return OC.imagePath('core','actions/history');
+ },
+ actionStub
+ );
+ fileList.fileActions.setDefault('text/plain', 'Test');
+ var $tr = fileList.findFileEl('One.txt');
+ $tr.find('td.filename>a.name').click();
+ expect(actionStub.calledOnce).toEqual(true);
+ expect(updateDetailsViewStub.notCalled).toEqual(true);
+ updateDetailsViewStub.restore();
+ });
+ it('Clicking on a file row will trigger details sidebar', function() {
+ fileList.fileActions.setDefault('text/plain', 'Test');
+ var $tr = fileList.findFileEl('One.txt');
+ $tr.find('td.filename>a.name').click();
+ expect($tr.hasClass('highlighted')).toEqual(true);
+
+ expect(fileList._detailsView.getFileInfo().id).toEqual(1);
+ });
+ it('Clicking outside to deselect a file row will trigger details sidebar', function() {
+ var $tr = fileList.findFileEl('One.txt');
+ $tr.find('td.filename>a.name').click();
+
+ fileList.$el.find('tfoot').click();
+
+ expect($tr.hasClass('highlighted')).toEqual(false);
+ expect(fileList._detailsView.getFileInfo()).toEqual(null);
+ });
+ });
describe('File actions', function() {
it('Clicking on a file name will trigger default action', function() {
var actionStub = sinon.stub();
@@ -1886,7 +1930,7 @@ describe('OCA.Files.FileList tests', function() {
);
fileList.fileActions.setDefault('text/plain', 'Test');
var $tr = fileList.findFileEl('One.txt');
- $tr.find('td.filename>a.name').click();
+ $tr.find('td.filename .nametext').click();
expect(actionStub.calledOnce).toEqual(true);
expect(actionStub.getCall(0).args[0]).toEqual('One.txt');
var context = actionStub.getCall(0).args[1];
diff --git a/apps/files/tests/js/mainfileinfodetailviewSpec.js b/apps/files/tests/js/mainfileinfodetailviewSpec.js
new file mode 100644
index 00000000000..10ad38097c6
--- /dev/null
+++ b/apps/files/tests/js/mainfileinfodetailviewSpec.js
@@ -0,0 +1,104 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2015 Vincent Petry <pvince81@owncloud.com>
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
+*
+*/
+
+describe('OCA.Files.MainFileInfoDetailView tests', function() {
+ var view, tooltipStub, previewStub, fncLazyLoadPreview, fileListMock;
+
+ beforeEach(function() {
+ tooltipStub = sinon.stub($.fn, 'tooltip');
+ fileListMock = sinon.mock(OCA.Files.FileList.prototype);
+ view = new OCA.Files.MainFileInfoDetailView();
+ });
+ afterEach(function() {
+ view.destroy();
+ view = undefined;
+ tooltipStub.restore();
+ fileListMock.restore();
+
+ });
+ describe('rendering', function() {
+ var testFileInfo;
+ beforeEach(function() {
+ view = new OCA.Files.MainFileInfoDetailView();
+ testFileInfo = {
+ id: 5,
+ name: 'One.txt',
+ path: '/subdir',
+ size: 123456789,
+ mtime: Date.UTC(2015, 6, 17, 1, 2, 0, 0)
+ };
+ });
+ it('displays basic info', function() {
+ var clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3));
+ var dateExpected = OC.Util.formatDate(Date(Date.UTC(2015, 6, 17, 1, 2, 0, 0)));
+ view.setFileInfo(testFileInfo);
+ expect(view.$el.find('.fileName').text()).toEqual('One.txt');
+ expect(view.$el.find('.fileName').attr('title')).toEqual('One.txt');
+ expect(view.$el.find('.size').text()).toEqual('117.7 MB');
+ expect(view.$el.find('.size').attr('title')).toEqual('123456789 bytes');
+ expect(view.$el.find('.date').text()).toEqual('a few seconds ago');
+ expect(view.$el.find('.date').attr('title')).toEqual(dateExpected);
+ clock.restore();
+ });
+ it('displays favorite icon', function() {
+ view.setFileInfo(_.extend(testFileInfo, {
+ tags: [OC.TAG_FAVORITE]
+ }));
+ expect(view.$el.find('.favorite img').attr('src'))
+ .toEqual(OC.imagePath('core', 'actions/starred'));
+
+ view.setFileInfo(_.extend(testFileInfo, {
+ tags: []
+ }));
+ expect(view.$el.find('.favorite img').attr('src'))
+ .toEqual(OC.imagePath('core', 'actions/star'));
+ });
+ it('displays mime icon', function() {
+ // File
+ view.setFileInfo(_.extend(testFileInfo, {
+ mimetype: 'text/calendar'
+ }));
+
+ expect(view.$el.find('.thumbnail').css('background-image'))
+ .toContain('filetypes/text-calendar.svg');
+
+ // Folder
+ view.setFileInfo(_.extend(testFileInfo, {
+ mimetype: 'httpd/unix-directory'
+ }));
+
+ expect(view.$el.find('.thumbnail').css('background-image'))
+ .toContain('filetypes/folder.svg');
+ });
+ it('displays thumbnail', function() {
+ view.setFileInfo(_.extend(testFileInfo, {
+ mimetype: 'text/plain'
+ }));
+
+ var expectation = fileListMock.expects('lazyLoadPreview');
+ expectation.once();
+
+ view.setFileInfo(testFileInfo);
+
+ fileListMock.verify();
+ });
+ });
+});
diff --git a/apps/files_external/appinfo/application.php b/apps/files_external/appinfo/application.php
index 62d4d142ba6..d77a302466c 100644
--- a/apps/files_external/appinfo/application.php
+++ b/apps/files_external/appinfo/application.php
@@ -21,7 +21,7 @@
*
*/
-namespace OCA\Files_External\Appinfo;
+namespace OCA\Files_External\AppInfo;
use \OCA\Files_External\Controller\AjaxController;
use \OCP\AppFramework\App;
diff --git a/apps/files_external/appinfo/routes.php b/apps/files_external/appinfo/routes.php
index 97eb1353b1e..bc4b0e98c91 100644
--- a/apps/files_external/appinfo/routes.php
+++ b/apps/files_external/appinfo/routes.php
@@ -23,7 +23,7 @@
*
*/
-namespace OCA\Files_External\Appinfo;
+namespace OCA\Files_External\AppInfo;
/**
* @var $this \OC\Route\Router
diff --git a/apps/files_external/lib/amazons3.php b/apps/files_external/lib/amazons3.php
index 02a02710a14..b956a607eba 100644
--- a/apps/files_external/lib/amazons3.php
+++ b/apps/files_external/lib/amazons3.php
@@ -40,6 +40,7 @@ require 'aws-autoloader.php';
use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
+use Icewind\Streams\IteratorDirectory;
class AmazonS3 extends \OC\Files\Storage\Common {
@@ -284,9 +285,7 @@ class AmazonS3 extends \OC\Files\Storage\Common {
$files[] = $file;
}
- \OC\Files\Stream\Dir::register('amazons3' . $path, $files);
-
- return opendir('fakedir://amazons3' . $path);
+ return IteratorDirectory::wrap($files);
} catch (S3Exception $e) {
\OCP\Util::logException('files_external', $e);
return false;
diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php
index 91e1aa7d509..8fcf39cc767 100644
--- a/apps/files_external/lib/config.php
+++ b/apps/files_external/lib/config.php
@@ -496,8 +496,16 @@ class OC_Mount_Config {
if (class_exists($class)) {
try {
$storage = new $class($options);
- if ($storage->test($isPersonal)) {
- return self::STATUS_SUCCESS;
+
+ try {
+ $result = $storage->test($isPersonal);
+ $storage->setAvailability($result);
+ if ($result) {
+ return self::STATUS_SUCCESS;
+ }
+ } catch (\Exception $e) {
+ $storage->setAvailability(false);
+ throw $e;
}
} catch (Exception $exception) {
\OCP\Util::logException('files_external', $exception);
diff --git a/apps/files_external/lib/dropbox.php b/apps/files_external/lib/dropbox.php
index 78219f8f06e..2d1aea1afc8 100644
--- a/apps/files_external/lib/dropbox.php
+++ b/apps/files_external/lib/dropbox.php
@@ -29,6 +29,8 @@
namespace OC\Files\Storage;
+use Icewind\Streams\IteratorDirectory;
+
require_once __DIR__ . '/../3rdparty/Dropbox/autoload.php';
class Dropbox extends \OC\Files\Storage\Common {
@@ -156,8 +158,7 @@ class Dropbox extends \OC\Files\Storage\Common {
foreach ($contents as $file) {
$files[] = basename($file['path']);
}
- \OC\Files\Stream\Dir::register('dropbox'.$path, $files);
- return opendir('fakedir://dropbox'.$path);
+ return IteratorDirectory::wrap($files);
}
return false;
}
diff --git a/apps/files_external/lib/google.php b/apps/files_external/lib/google.php
index 8199d97eacb..2ca550dfe7c 100644
--- a/apps/files_external/lib/google.php
+++ b/apps/files_external/lib/google.php
@@ -32,6 +32,8 @@
namespace OC\Files\Storage;
+use Icewind\Streams\IteratorDirectory;
+
set_include_path(get_include_path().PATH_SEPARATOR.
\OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
require_once 'Google/Client.php';
@@ -291,8 +293,7 @@ class Google extends \OC\Files\Storage\Common {
}
$pageToken = $children->getNextPageToken();
}
- \OC\Files\Stream\Dir::register('google'.$path, $files);
- return opendir('fakedir://google'.$path);
+ return IteratorDirectory::wrap($files);
} else {
return false;
}
diff --git a/apps/files_external/lib/sftp.php b/apps/files_external/lib/sftp.php
index 03ece9cb9dd..7f921b5342f 100644
--- a/apps/files_external/lib/sftp.php
+++ b/apps/files_external/lib/sftp.php
@@ -29,9 +29,8 @@
*
*/
namespace OC\Files\Storage;
+use Icewind\Streams\IteratorDirectory;
-use phpseclib\Net\RSA;
-use phpseclib\Net\SFTP;
use phpseclib\Net\SFTP\Stream;
/**
@@ -91,7 +90,7 @@ class SFTP extends \OC\Files\Storage\Common {
/**
* Returns the connection.
*
- * @return SFTP connected client instance
+ * @return \phpseclib\Net\SFTP connected client instance
* @throws \Exception when the connection failed
*/
public function getConnection() {
@@ -100,7 +99,7 @@ class SFTP extends \OC\Files\Storage\Common {
}
$hostKeys = $this->readHostKeys();
- $this->client = new SFTP($this->host, $this->port);
+ $this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
// The SSH Host Key MUST be verified before login().
$currentHostKey = $this->client->getServerPublicHostKey();
@@ -282,8 +281,7 @@ class SFTP extends \OC\Files\Storage\Common {
$dirStream[] = $file;
}
}
- \OC\Files\Stream\Dir::register($id, $dirStream);
- return opendir('fakedir://' . $id);
+ return IteratorDirectory::wrap($dirStream);
} catch(\Exception $e) {
return false;
}
diff --git a/apps/files_external/lib/sftp_key.php b/apps/files_external/lib/sftp_key.php
index 06771d57d22..a193b323678 100644
--- a/apps/files_external/lib/sftp_key.php
+++ b/apps/files_external/lib/sftp_key.php
@@ -23,12 +23,14 @@
namespace OC\Files\Storage;
use phpseclib\Crypt\RSA;
-use phpseclib\Net\SFTP;
class SFTP_Key extends \OC\Files\Storage\SFTP {
private $publicKey;
private $privateKey;
+ /**
+ * {@inheritdoc}
+ */
public function __construct($params) {
parent::__construct($params);
$this->publicKey = $params['public_key'];
@@ -38,7 +40,7 @@ class SFTP_Key extends \OC\Files\Storage\SFTP {
/**
* Returns the connection.
*
- * @return SFTP connected client instance
+ * @return \phpseclib\Net\SFTP connected client instance
* @throws \Exception when the connection failed
*/
public function getConnection() {
@@ -47,7 +49,7 @@ class SFTP_Key extends \OC\Files\Storage\SFTP {
}
$hostKeys = $this->readHostKeys();
- $this->client = new SFTP($this->getHost());
+ $this->client = new \phpseclib\Net\SFTP($this->getHost());
// The SSH Host Key MUST be verified before login().
$currentHostKey = $this->client->getServerPublicHostKey();
diff --git a/apps/files_external/lib/streamwrapper.php b/apps/files_external/lib/streamwrapper.php
index f2438a5487b..387667a81a9 100644
--- a/apps/files_external/lib/streamwrapper.php
+++ b/apps/files_external/lib/streamwrapper.php
@@ -40,8 +40,11 @@ abstract class StreamWrapper extends Common {
}
public function rmdir($path) {
- if ($this->file_exists($path) && $this->isDeletable($path)) {
+ if ($this->is_dir($path) && $this->isDeletable($path)) {
$dh = $this->opendir($path);
+ if (!is_resource($dh)) {
+ return false;
+ }
while (($file = readdir($dh)) !== false) {
if ($this->is_dir($path . '/' . $file)) {
$this->rmdir($path . '/' . $file);
diff --git a/apps/files_external/lib/swift.php b/apps/files_external/lib/swift.php
index 50f0d40805a..d8107e58fed 100644
--- a/apps/files_external/lib/swift.php
+++ b/apps/files_external/lib/swift.php
@@ -32,6 +32,7 @@
namespace OC\Files\Storage;
use Guzzle\Http\Exception\ClientErrorResponseException;
+use Icewind\Streams\IteratorDirectory;
use OpenCloud;
use OpenCloud\Common\Exceptions;
use OpenCloud\OpenStack;
@@ -222,8 +223,7 @@ class Swift extends \OC\Files\Storage\Common {
}
}
- \OC\Files\Stream\Dir::register('swift' . $path, $files);
- return opendir('fakedir://swift' . $path);
+ return IteratorDirectory::wrap($files);
} catch (\Exception $e) {
\OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
return false;
diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php
index f72f5024622..9000fafd8dd 100644
--- a/apps/files_sharing/appinfo/app.php
+++ b/apps/files_sharing/appinfo/app.php
@@ -56,6 +56,7 @@ $application->setupPropagation();
\OCP\Util::addScript('files_sharing', 'share');
\OCP\Util::addScript('files_sharing', 'external');
+\OCP\Util::addStyle('files_sharing', 'sharetabview');
// FIXME: registering a job here will cause additional useless SQL queries
// when the route is not cron.php, needs a better way
diff --git a/apps/files_sharing/appinfo/application.php b/apps/files_sharing/appinfo/application.php
index b9c2844d78c..2fe9019d54e 100644
--- a/apps/files_sharing/appinfo/application.php
+++ b/apps/files_sharing/appinfo/application.php
@@ -2,6 +2,7 @@
/**
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <icewind@owncloud.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
@@ -20,7 +21,7 @@
*
*/
-namespace OCA\Files_Sharing\Appinfo;
+namespace OCA\Files_Sharing\AppInfo;
use OCA\Files_Sharing\Helper;
use OCA\Files_Sharing\MountProvider;
@@ -31,6 +32,7 @@ use OCA\Files_Sharing\Controllers\ExternalSharesController;
use OCA\Files_Sharing\Controllers\ShareController;
use OCA\Files_Sharing\Middleware\SharingCheckMiddleware;
use \OCP\IContainer;
+use OCA\Files_Sharing\Capabilities;
class Application extends App {
public function __construct(array $urlParams = array()) {
@@ -122,6 +124,11 @@ class Application extends App {
$server->getConfig()
);
});
+
+ /*
+ * Register capabilities
+ */
+ $container->registerCapability('OCA\Files_Sharing\Capabilities');
}
public function registerMountProviders() {
diff --git a/apps/files_sharing/appinfo/routes.php b/apps/files_sharing/appinfo/routes.php
index 21d21a83441..1e99267a43a 100644
--- a/apps/files_sharing/appinfo/routes.php
+++ b/apps/files_sharing/appinfo/routes.php
@@ -97,8 +97,3 @@ API::register('delete',
array('\OCA\Files_Sharing\API\Remote', 'declineShare'),
'files_sharing');
-// Register with the capabilities API
-API::register('get',
- '/cloud/capabilities',
- array('OCA\Files_Sharing\Capabilities', 'getCapabilities'),
- 'files_sharing', API::USER_AUTH);
diff --git a/apps/files_sharing/css/sharetabview.css b/apps/files_sharing/css/sharetabview.css
new file mode 100644
index 00000000000..42c9bee7173
--- /dev/null
+++ b/apps/files_sharing/css/sharetabview.css
@@ -0,0 +1,3 @@
+.app-files .shareTabView {
+ min-height: 100px;
+}
diff --git a/apps/files_sharing/js/public.js b/apps/files_sharing/js/public.js
index 5923e426f05..1993efe7d73 100644
--- a/apps/files_sharing/js/public.js
+++ b/apps/files_sharing/js/public.js
@@ -57,7 +57,8 @@ OCA.Sharing.PublicApp = {
scrollContainer: $(window),
dragOptions: dragOptions,
folderDropOptions: folderDropOptions,
- fileActions: fileActions
+ fileActions: fileActions,
+ detailsViewEnabled: false
}
);
this.files = OCA.Files.Files;
diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js
index e7823454c53..12bec0e8c9a 100644
--- a/apps/files_sharing/js/share.js
+++ b/apps/files_sharing/js/share.js
@@ -140,6 +140,10 @@
}
});
}, t('files_sharing', 'Share'));
+
+ OC.addScript('files_sharing', 'sharetabview').done(function() {
+ fileList.registerTabView(new OCA.Sharing.ShareTabView('shareTabView'));
+ });
},
/**
diff --git a/apps/files_sharing/js/sharetabview.js b/apps/files_sharing/js/sharetabview.js
new file mode 100644
index 00000000000..e02de923751
--- /dev/null
+++ b/apps/files_sharing/js/sharetabview.js
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ var TEMPLATE =
+ '<div>Owner: {{owner}}';
+
+ /**
+ * @class OCA.Sharing.ShareTabView
+ * @classdesc
+ *
+ * Displays sharing information
+ *
+ */
+ var ShareTabView = function(id) {
+ this.initialize(id);
+ };
+ /**
+ * @memberof OCA.Sharing
+ */
+ ShareTabView.prototype = _.extend({}, OCA.Files.DetailTabView.prototype,
+ /** @lends OCA.Sharing.ShareTabView.prototype */ {
+ _template: null,
+
+ /**
+ * Initialize the details view
+ */
+ initialize: function() {
+ OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
+ this.$el.addClass('shareTabView');
+ },
+
+ getLabel: function() {
+ return t('files_sharing', 'Sharing');
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ this.$el.empty();
+
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ if (this._fileInfo) {
+ this.$el.append(this._template({
+ owner: this._fileInfo.shareOwner || OC.currentUser
+ }));
+
+ } else {
+ // TODO: render placeholder text?
+ }
+ }
+ });
+
+ OCA.Sharing.ShareTabView = ShareTabView;
+})();
+
diff --git a/apps/files_sharing/lib/cache.php b/apps/files_sharing/lib/cache.php
index bb62e8078ad..c25dc92409f 100644
--- a/apps/files_sharing/lib/cache.php
+++ b/apps/files_sharing/lib/cache.php
@@ -60,7 +60,7 @@ class Shared_Cache extends Cache {
if ($target === false || $target === $this->storage->getMountPoint()) {
$target = '';
}
- $source = \OC_Share_Backend_File::getSource($target, $this->storage->getMountPoint(), $this->storage->getItemType());
+ $source = \OC_Share_Backend_File::getSource($target, $this->storage->getShare());
if (isset($source['path']) && isset($source['fileOwner'])) {
\OC\Files\Filesystem::initMountPoints($source['fileOwner']);
$mounts = \OC\Files\Filesystem::getMountByNumericId($source['storage']);
@@ -242,7 +242,7 @@ class Shared_Cache extends Cache {
*/
protected function getMoveInfo($path) {
$cache = $this->getSourceCache($path);
- $file = \OC_Share_Backend_File::getSource($path, $this->storage->getMountPoint(), $this->storage->getItemType());
+ $file = \OC_Share_Backend_File::getSource($path, $this->storage->getShare());
return [$cache->getNumericStorageId(), $file['path']];
}
diff --git a/apps/files_sharing/lib/capabilities.php b/apps/files_sharing/lib/capabilities.php
index ac6454c3433..ef69a40078b 100644
--- a/apps/files_sharing/lib/capabilities.php
+++ b/apps/files_sharing/lib/capabilities.php
@@ -20,6 +20,7 @@
*/
namespace OCA\Files_Sharing;
+use OCP\Capabilities\ICapability;
use \OCP\IConfig;
/**
@@ -27,32 +28,21 @@ use \OCP\IConfig;
*
* @package OCA\Files_Sharing
*/
-class Capabilities {
+class Capabilities implements ICapability {
/** @var IConfig */
private $config;
- /**
- * @param IConfig $config
- */
public function __construct(IConfig $config) {
$this->config = $config;
}
/**
- * @return \OC_OCS_Result
- */
- public static function getCapabilities() {
- $config = \OC::$server->getConfig();
- $cap = new Capabilities($config);
- return $cap->getCaps();
- }
-
-
- /**
- * @return \OC_OCS_Result
+ * Return this classes capabilities
+ *
+ * @return array
*/
- public function getCaps() {
+ public function getCapabilities() {
$res = [];
$public = [];
@@ -76,12 +66,8 @@ class Capabilities {
$res['resharing'] = $this->config->getAppValue('core', 'shareapi_allow_resharing', 'yes') === 'yes';
-
- return new \OC_OCS_Result([
- 'capabilities' => [
- 'files_sharing' => $res
- ],
- ]);
+ return [
+ 'files_sharing' => $res,
+ ];
}
-
}
diff --git a/apps/files_sharing/lib/share/file.php b/apps/files_sharing/lib/share/file.php
index 9c09e05408b..7bbc2083702 100644
--- a/apps/files_sharing/lib/share/file.php
+++ b/apps/files_sharing/lib/share/file.php
@@ -206,27 +206,15 @@ class OC_Share_Backend_File implements OCP\Share_Backend_File_Dependent {
/**
* @param string $target
- * @param string $mountPoint
- * @param string $itemType
+ * @param array $share
* @return array|false source item
*/
- public static function getSource($target, $mountPoint, $itemType) {
- if ($itemType === 'folder') {
- $source = \OCP\Share::getItemSharedWith('folder', $mountPoint, \OC_Share_Backend_File::FORMAT_SHARED_STORAGE);
- if ($source && $target !== '') {
- // note: in case of ext storage mount points the path might be empty
- // which would cause a leading slash to appear
- $source['path'] = ltrim($source['path'] . '/' . $target, '/');
- }
- } else {
- $source = \OCP\Share::getItemSharedWith('file', $mountPoint, \OC_Share_Backend_File::FORMAT_SHARED_STORAGE);
- }
- if ($source) {
- return self::resolveReshares($source);
+ public static function getSource($target, $share) {
+ if ($share['item_type'] === 'folder' && $target !== '') {
+ // note: in case of ext storage mount points the path might be empty
+ // which would cause a leading slash to appear
+ $share['path'] = ltrim($share['path'] . '/' . $target, '/');
}
-
- \OCP\Util::writeLog('files_sharing', 'File source not found for: '.$target, \OCP\Util::DEBUG);
- return false;
+ return self::resolveReshares($share);
}
-
}
diff --git a/apps/files_sharing/lib/sharedstorage.php b/apps/files_sharing/lib/sharedstorage.php
index ff01489d77b..66803db1425 100644
--- a/apps/files_sharing/lib/sharedstorage.php
+++ b/apps/files_sharing/lib/sharedstorage.php
@@ -83,14 +83,14 @@ class Shared extends \OC\Files\Storage\Common implements ISharedStorage {
if (!isset($this->files[$target])) {
// Check for partial files
if (pathinfo($target, PATHINFO_EXTENSION) === 'part') {
- $source = \OC_Share_Backend_File::getSource(substr($target, 0, -5), $this->getMountPoint(), $this->getItemType());
+ $source = \OC_Share_Backend_File::getSource(substr($target, 0, -5), $this->getShare());
if ($source) {
$source['path'] .= '.part';
// All partial files have delete permission
$source['permissions'] |= \OCP\Constants::PERMISSION_DELETE;
}
} else {
- $source = \OC_Share_Backend_File::getSource($target, $this->getMountPoint(), $this->getItemType());
+ $source = \OC_Share_Backend_File::getSource($target, $this->getShare());
}
$this->files[$target] = $source;
}
diff --git a/apps/files_sharing/tests/capabilities.php b/apps/files_sharing/tests/capabilities.php
index a7c487bf589..b0f6390b013 100644
--- a/apps/files_sharing/tests/capabilities.php
+++ b/apps/files_sharing/tests/capabilities.php
@@ -36,9 +36,8 @@ class FilesSharingCapabilitiesTest extends \Test\TestCase {
* @return string[]
*/
private function getFilesSharingPart(array $data) {
- $this->assertArrayHasKey('capabilities', $data);
- $this->assertArrayHasKey('files_sharing', $data['capabilities']);
- return $data['capabilities']['files_sharing'];
+ $this->assertArrayHasKey('files_sharing', $data);
+ return $data['files_sharing'];
}
/**
@@ -53,7 +52,7 @@ class FilesSharingCapabilitiesTest extends \Test\TestCase {
$stub = $this->getMockBuilder('\OCP\IConfig')->disableOriginalConstructor()->getMock();
$stub->method('getAppValue')->will($this->returnValueMap($map));
$cap = new Capabilities($stub);
- $result = $this->getFilesSharingPart($cap->getCaps()->getData());
+ $result = $this->getFilesSharingPart($cap->getCapabilities());
return $result;
}
diff --git a/apps/files_sharing/tests/js/appSpec.js b/apps/files_sharing/tests/js/appSpec.js
index 49bca568001..133bd44f750 100644
--- a/apps/files_sharing/tests/js/appSpec.js
+++ b/apps/files_sharing/tests/js/appSpec.js
@@ -132,7 +132,7 @@ describe('OCA.Sharing.App tests', function() {
shareOwner: 'user2'
}]);
- fileListIn.findFileEl('testdir').find('td a.name').click();
+ fileListIn.findFileEl('testdir').find('td .nametext').click();
expect(OCA.Files.App.fileList.getCurrentDirectory()).toEqual('/somewhere/inside/subdir/testdir');
diff --git a/apps/files_sharing/tests/sharedstorage.php b/apps/files_sharing/tests/sharedstorage.php
index 7c28d0431e1..de510cf1eec 100644
--- a/apps/files_sharing/tests/sharedstorage.php
+++ b/apps/files_sharing/tests/sharedstorage.php
@@ -441,4 +441,43 @@ class Test_Files_Sharing_Storage extends OCA\Files_sharing\Tests\TestCase {
self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
$this->view->unlink($this->folder);
}
+
+ public function testNameConflict() {
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
+ $view1 = new \OC\Files\View('/' . self::TEST_FILES_SHARING_API_USER1 . '/files');
+ $view1->mkdir('foo');
+ $folderInfo1 = $view1->getFileInfo('foo');
+
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
+ $view3 = new \OC\Files\View('/' . self::TEST_FILES_SHARING_API_USER3 . '/files');
+ $view3->mkdir('foo');
+ $folderInfo2 = $view3->getFileInfo('foo');
+
+ // share a folder with the same name from two different users to the same user
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER1);
+
+ \OCP\Share::shareItem('folder', $folderInfo1['fileid'], \OCP\Share::SHARE_TYPE_GROUP,
+ self::TEST_FILES_SHARING_API_GROUP1, 31);
+
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
+
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER3);
+
+ \OCP\Share::shareItem('folder', $folderInfo2['fileid'], \OCP\Share::SHARE_TYPE_GROUP,
+ self::TEST_FILES_SHARING_API_GROUP1, 31);
+
+ self::loginHelper(self::TEST_FILES_SHARING_API_USER2);
+ $view2 = new \OC\Files\View('/' . self::TEST_FILES_SHARING_API_USER2 . '/files');
+
+ $this->assertTrue($view2->file_exists('/foo'));
+ $this->assertTrue($view2->file_exists('/foo (2)'));
+
+ $mount = $view2->getMount('/foo');
+ $this->assertInstanceOf('\OCA\Files_Sharing\SharedMount', $mount);
+ /** @var \OC\Files\Storage\Shared $storage */
+ $storage = $mount->getStorage();
+
+ $source = $storage->getFile('');
+ $this->assertEquals(self::TEST_FILES_SHARING_API_USER1, $source['uid_owner']);
+ }
}
diff --git a/apps/files_trashbin/appinfo/application.php b/apps/files_trashbin/appinfo/application.php
new file mode 100644
index 00000000000..8d76d40f639
--- /dev/null
+++ b/apps/files_trashbin/appinfo/application.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Files_Trashbin\AppInfo;
+
+use OCP\AppFramework\App;
+
+class Application extends App {
+ public function __construct(array $urlParams = array()) {
+ parent::__construct('files_trashbin', $urlParams);
+
+ $container = $this->getContainer();
+
+ /*
+ * Register capabilities
+ */
+ $container->registerCapability('OCA\Files_Trashbin\Capabilities');
+ }
+}
diff --git a/apps/files_trashbin/appinfo/routes.php b/apps/files_trashbin/appinfo/routes.php
index 99a03d6b969..cf3d7b77ec2 100644
--- a/apps/files_trashbin/appinfo/routes.php
+++ b/apps/files_trashbin/appinfo/routes.php
@@ -1,9 +1,8 @@
<?php
/**
* @author Georg Ehrke <georg@owncloud.com>
- * @author Joas Schilling <nickvergessen@owncloud.com>
* @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <pvince81@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
@@ -22,6 +21,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
+
+namespace OCA\Files_Trashbin\AppInfo;
+
+$application = new Application();
+
$this->create('core_ajax_trashbin_preview', 'ajax/preview.php')
->actionInclude('files_trashbin/ajax/preview.php');
$this->create('files_trashbin_ajax_delete', 'ajax/delete.php')
@@ -33,6 +37,3 @@ $this->create('files_trashbin_ajax_list', 'ajax/list.php')
$this->create('files_trashbin_ajax_undelete', 'ajax/undelete.php')
->actionInclude('files_trashbin/ajax/undelete.php');
-
-// Register with the capabilities API
-\OCP\API::register('get', '/cloud/capabilities', array('OCA\Files_Trashbin\Capabilities', 'getCapabilities'), 'files_trashbin', \OCP\API::USER_AUTH);
diff --git a/apps/files_trashbin/command/cleanup.php b/apps/files_trashbin/command/cleanup.php
index 0cc94912339..60717abac18 100644
--- a/apps/files_trashbin/command/cleanup.php
+++ b/apps/files_trashbin/command/cleanup.php
@@ -108,7 +108,7 @@ class CleanUp extends Command {
if ($this->rootFolder->nodeExists('/' . $uid . '/files_trashbin')) {
$this->rootFolder->get('/' . $uid . '/files_trashbin')->delete();
$query = $this->dbConnection->getQueryBuilder();
- $query->delete('*PREFIX*files_trash')
+ $query->delete('files_trash')
->where($query->expr()->eq('user', $query->createParameter('uid')))
->setParameter('uid', $uid);
$query->execute();
diff --git a/apps/files_trashbin/lib/capabilities.php b/apps/files_trashbin/lib/capabilities.php
index 89b268489b5..c991cc8be65 100644
--- a/apps/files_trashbin/lib/capabilities.php
+++ b/apps/files_trashbin/lib/capabilities.php
@@ -2,6 +2,7 @@
/**
* @author Lukas Reschke <lukas@owncloud.com>
* @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
@@ -22,25 +23,26 @@
namespace OCA\Files_Trashbin;
+use OCP\Capabilities\ICapability;
/**
* Class Capabilities
*
* @package OCA\Files_Trashbin
*/
-class Capabilities {
+class Capabilities implements ICapability {
/**
- * @return \OC_OCS_Result
+ * Return this classes capabilities
+ *
+ * @return array
*/
- public static function getCapabilities() {
- return new \OC_OCS_Result(array(
- 'capabilities' => array(
- 'files' => array(
- 'undelete' => true,
- ),
- ),
- ));
+ public function getCapabilities() {
+ return [
+ 'files' => [
+ 'undelete' => true
+ ]
+ ];
}
}
diff --git a/apps/files_trashbin/lib/helper.php b/apps/files_trashbin/lib/helper.php
index 42412d5d4c9..f51185712a9 100644
--- a/apps/files_trashbin/lib/helper.php
+++ b/apps/files_trashbin/lib/helper.php
@@ -83,7 +83,7 @@ class Helper
$i = array(
'name' => $id,
'mtime' => $timestamp,
- 'mimetype' => \OC_Helper::getFileNameMimeType($id),
+ 'mimetype' => $view->is_dir($dir . '/' . $entryName) ? 'httpd/unix-directory' : \OC_Helper::getFileNameMimeType($id),
'type' => $view->is_dir($dir . '/' . $entryName) ? 'dir' : 'file',
'directory' => ($dir === '/') ? '' : $dir,
);
diff --git a/apps/files_trashbin/tests/command/cleanuptest.php b/apps/files_trashbin/tests/command/cleanuptest.php
index a7400e901fa..d4cccee448e 100644
--- a/apps/files_trashbin/tests/command/cleanuptest.php
+++ b/apps/files_trashbin/tests/command/cleanuptest.php
@@ -43,7 +43,7 @@ class CleanUpTest extends TestCase {
protected $dbConnection;
/** @var string */
- protected $trashTable = '*PREFIX*files_trash';
+ protected $trashTable = 'files_trash';
/** @var string */
protected $user0 = 'user0';
diff --git a/apps/files_versions/appinfo/application.php b/apps/files_versions/appinfo/application.php
new file mode 100644
index 00000000000..bab36b48510
--- /dev/null
+++ b/apps/files_versions/appinfo/application.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Files_Versions\AppInfo;
+
+use OCP\AppFramework\App;
+
+class Application extends App {
+ public function __construct(array $urlParams = array()) {
+ parent::__construct('files_versions', $urlParams);
+
+ $container = $this->getContainer();
+
+ /*
+ * Register capabilities
+ */
+ $container->registerCapability('OCA\Files_Versions\Capabilities');
+ }
+}
diff --git a/apps/files_versions/appinfo/routes.php b/apps/files_versions/appinfo/routes.php
index 5dbed1f93eb..9bab86d9224 100644
--- a/apps/files_versions/appinfo/routes.php
+++ b/apps/files_versions/appinfo/routes.php
@@ -1,10 +1,10 @@
<?php
/**
* @author Björn Schießle <schiessle@owncloud.com>
- * @author Joas Schilling <nickvergessen@owncloud.com>
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
* @author Lukas Reschke <lukas@owncloud.com>
* @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tom Needham <tom@owncloud.com>
*
@@ -25,6 +25,10 @@
*
*/
+namespace OCA\Files_Versions\AppInfo;
+
+$application = new Application();
+
/** @var $this \OCP\Route\IRouter */
$this->create('core_ajax_versions_preview', '/preview')->action(
function() {
@@ -38,5 +42,3 @@ $this->create('files_versions_ajax_getVersions', 'ajax/getVersions.php')
$this->create('files_versions_ajax_rollbackVersion', 'ajax/rollbackVersion.php')
->actionInclude('files_versions/ajax/rollbackVersion.php');
-// Register with the capabilities API
-\OCP\API::register('get', '/cloud/capabilities', array('OCA\Files_Versions\Capabilities', 'getCapabilities'), 'files_versions', \OCP\API::USER_AUTH);
diff --git a/apps/files_versions/lib/capabilities.php b/apps/files_versions/lib/capabilities.php
index aea31b25240..11b98038f46 100644
--- a/apps/files_versions/lib/capabilities.php
+++ b/apps/files_versions/lib/capabilities.php
@@ -2,6 +2,7 @@
/**
* @author Christopher Schäpers <kondou@ts.unde.re>
* @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Tom Needham <tom@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
@@ -23,16 +24,20 @@
namespace OCA\Files_Versions;
-class Capabilities {
+use OCP\Capabilities\ICapability;
+
+class Capabilities implements ICapability {
- public static function getCapabilities() {
- return new \OC_OCS_Result(array(
- 'capabilities' => array(
- 'files' => array(
- 'versioning' => true,
- ),
- ),
- ));
+ /**
+ * Return this classes capabilities
+ *
+ * @return array
+ */
+ public function getCapabilities() {
+ return [
+ 'files' => [
+ 'versioning' => true
+ ]
+ ];
}
-
}
diff --git a/apps/user_ldap/appinfo/update.php b/apps/user_ldap/appinfo/update.php
new file mode 100644
index 00000000000..b904bce072e
--- /dev/null
+++ b/apps/user_ldap/appinfo/update.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * @author Morris Jobke <hey@morrisjobke.de>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+$installedVersion = \OC::$server->getConfig()->getAppValue('user_ldap', 'installed_version');
+
+if (version_compare($installedVersion, '0.6.1', '<')) {
+ \OC::$server->getConfig()->setAppValue('user_ldap', 'enforce_home_folder_naming_rule', false);
+}
diff --git a/apps/user_ldap/tests/integration/lib/integrationtestuserhome.php b/apps/user_ldap/tests/integration/lib/integrationtestuserhome.php
new file mode 100644
index 00000000000..f34fca81c2d
--- /dev/null
+++ b/apps/user_ldap/tests/integration/lib/integrationtestuserhome.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: blizzz
+ * Date: 06.08.15
+ * Time: 08:19
+ */
+
+namespace OCA\user_ldap\tests\integration\lib;
+
+use OCA\user_ldap\lib\user\Manager as LDAPUserManager;
+use OCA\user_ldap\tests\integration\AbstractIntegrationTest;
+use OCA\User_LDAP\Mapping\UserMapping;
+use OCA\user_ldap\USER_LDAP;
+
+require_once __DIR__ . '/../../../../../lib/base.php';
+
+class IntegrationTestUserHome extends AbstractIntegrationTest {
+ /** @var UserMapping */
+ protected $mapping;
+
+ /** @var USER_LDAP */
+ protected $backend;
+
+ /**
+ * prepares the LDAP environment and sets up a test configuration for
+ * the LDAP backend.
+ */
+ public function init() {
+ require(__DIR__ . '/../setup-scripts/createExplicitUsers.php');
+ parent::init();
+
+ $this->mapping = new UserMapping(\OC::$server->getDatabaseConnection());
+ $this->mapping->clear();
+ $this->access->setUserMapper($this->mapping);
+ $this->backend = new \OCA\user_ldap\USER_LDAP($this->access, \OC::$server->getConfig());
+ }
+
+ /**
+ * sets up the LDAP configuration to be used for the test
+ */
+ protected function initConnection() {
+ parent::initConnection();
+ $this->connection->setConfiguration([
+ 'homeFolderNamingRule' => 'homeDirectory',
+ ]);
+ }
+
+ /**
+ * initializes an LDAP user manager instance
+ * @return LDAPUserManager
+ */
+ protected function initUserManager() {
+ $this->userManager = new LDAPUserManager(
+ \OC::$server->getConfig(),
+ new \OCA\user_ldap\lib\FilesystemHelper(),
+ new \OCA\user_ldap\lib\LogWrapper(),
+ \OC::$server->getAvatarManager(),
+ new \OCP\Image(),
+ \OC::$server->getDatabaseConnection()
+ );
+ }
+
+ /**
+ * homeDirectory on LDAP is empty. Return values of getHome should be
+ * identical to user name, following ownCloud default.
+ *
+ * @return bool
+ */
+ protected function case1() {
+ \OC::$server->getConfig()->setAppValue('user_ldap', 'enforce_home_folder_naming_rule', false);
+ $userManager = \oc::$server->getUserManager();
+ $userManager->clearBackends();
+ $userManager->registerBackend($this->backend);
+ $users = $userManager->search('', 5, 0);
+
+ foreach($users as $user) {
+ $home = $user->getHome();
+ $uid = $user->getUID();
+ $posFound = strpos($home, '/' . $uid);
+ $posExpected = strlen($home) - (strlen($uid) + 1);
+ if($posFound === false || $posFound !== $posExpected) {
+ print('"' . $user->getUID() . '" was not found in "' . $home . '" or does not end with it.' . PHP_EOL);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * homeDirectory on LDAP is empty. Having the attributes set is enforced.
+ *
+ * @return bool
+ */
+ protected function case2() {
+ \OC::$server->getConfig()->setAppValue('user_ldap', 'enforce_home_folder_naming_rule', true);
+ $userManager = \oc::$server->getUserManager();
+ // clearing backends is critical, otherwise the userManager will have
+ // the user objects cached and the value from case1 returned
+ $userManager->clearBackends();
+ $userManager->registerBackend($this->backend);
+ $users = $userManager->search('', 5, 0);
+
+ try {
+ foreach ($users as $user) {
+ $user->getHome();
+ print('User home was retrieved without throwing an Exception!' . PHP_EOL);
+ return false;
+ }
+ } catch (\Exception $e) {
+ if(strpos($e->getMessage(), 'Home dir attribute') === 0) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * homeDirectory on LDAP is set to "attr:" which is effectively empty.
+ * Return values of getHome should be ownCloud default.
+ *
+ * @return bool
+ */
+ protected function case3() {
+ \OC::$server->getConfig()->setAppValue('user_ldap', 'enforce_home_folder_naming_rule', true);
+ $this->connection->setConfiguration([
+ 'homeFolderNamingRule' => 'attr:',
+ ]);
+ $userManager = \oc::$server->getUserManager();
+ $userManager->clearBackends();
+ $userManager->registerBackend($this->backend);
+ $users = $userManager->search('', 5, 0);
+
+ try {
+ foreach ($users as $user) {
+ $home = $user->getHome();
+ $uid = $user->getUID();
+ $posFound = strpos($home, '/' . $uid);
+ $posExpected = strlen($home) - (strlen($uid) + 1);
+ if ($posFound === false || $posFound !== $posExpected) {
+ print('"' . $user->getUID() . '" was not found in "' . $home . '" or does not end with it.' . PHP_EOL);
+ return false;
+ }
+ }
+ } catch (\Exception $e) {
+ print("Unexpected Exception: " . $e->getMessage() . PHP_EOL);
+ return false;
+ }
+
+ return true;
+ }
+}
+
+require_once(__DIR__ . '/../setup-scripts/config.php');
+$test = new IntegrationTestUserHome($host, $port, $adn, $apwd, $bdn);
+$test->init();
+$test->run();
diff --git a/apps/user_ldap/user_ldap.php b/apps/user_ldap/user_ldap.php
index caff30a0e60..a2f4b4ee9e5 100644
--- a/apps/user_ldap/user_ldap.php
+++ b/apps/user_ldap/user_ldap.php
@@ -266,7 +266,8 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn
if($this->access->connection->isCached($cacheKey)) {
return $this->access->connection->getFromCache($cacheKey);
}
- if(strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0) {
+ if(strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0 &&
+ $this->access->connection->homeFolderNamingRule !== 'attr:') {
$attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
$homedir = $this->access->readAttribute(
$this->access->username2dn($uid), $attr);
@@ -293,6 +294,10 @@ class USER_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserIn
//TODO: if home directory changes, the old one needs to be removed.
return $homedir;
}
+ if($this->ocConfig->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true)) {
+ // a naming rule attribute is defined, but it doesn't exist for that LDAP user
+ throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $uid);
+ }
}
//false will apply default behaviour as defined and done by OC_User
diff --git a/bower.json b/bower.json
index 112a9a907cd..1fe270d9f3c 100644
--- a/bower.json
+++ b/bower.json
@@ -26,6 +26,7 @@
"snapjs": "~2.0.0-rc1",
"strengthify": "0.4.1",
"underscore": "~1.8.0",
- "bootstrap": "~3.3.5"
+ "bootstrap": "~3.3.5",
+ "backbone": "~1.2.1"
}
}
diff --git a/core/css/apps.css b/core/css/apps.css
index 57133729f15..5769120c5ed 100644
--- a/core/css/apps.css
+++ b/core/css/apps.css
@@ -417,7 +417,39 @@
min-height: 100%;
}
+/* APP-SIDEBAR ----------------------------------------------------------------*/
+
+/*
+ Sidebar: a sidebar to be used within #app-content
+ have it as first element within app-content in order to shrink other
+ sibling containers properly. Compare Files app for example.
+*/
+#app-sidebar {
+ position: fixed;
+ top: 45px;
+ right: 0;
+ left: auto;
+ bottom: 0;
+ width: 27%;
+ display: block;
+ background: #eee;
+ -webkit-transition: margin-right 300ms;
+ -moz-transition: margin-right 300ms;
+ -o-transition: margin-right 300ms;
+ transition: margin-right 300ms;
+ overflow-x: hidden;
+ overflow-y: auto;
+ visibility: visible;
+ z-index: 500;
+}
+#app-content.with-app-sidebar {
+ margin-right: 27%;
+}
+
+#app-sidebar.disappear {
+ visibility: hidden;
+}
/* APP-SETTINGS ---------------------------------------------------------------*/
@@ -556,3 +588,50 @@ em {
padding:16px;
}
+/* generic tab styles */
+.tabHeaders {
+ margin: 15px;
+ background-color: #1D2D44;
+}
+
+.tabHeaders .tabHeader {
+ float: left;
+ border: 1px solid #ddd;
+ padding: 5px;
+ cursor: pointer;
+ background-color: #f8f8f8;
+ font-weight: bold;
+}
+.tabHeaders .tabHeader, .tabHeaders .tabHeader a {
+ color: #888;
+}
+
+.tabHeaders .tabHeader:first-child {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.tabHeaders .tabHeader:last-child {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.tabHeaders .tabHeader.selected,
+.tabHeaders .tabHeader:hover {
+ background-color: #e8e8e8;
+}
+
+.tabHeaders .tabHeader.selected,
+.tabHeaders .tabHeader.selected a,
+.tabHeaders .tabHeader:hover,
+.tabHeaders .tabHeader:hover a {
+ color: #000;
+}
+
+.tabsContainer {
+ clear: left;
+}
+
+.tabsContainer .tab {
+ padding: 15px;
+}
diff --git a/core/css/mobile.css b/core/css/mobile.css
index 80217d7069c..2256d821d73 100644
--- a/core/css/mobile.css
+++ b/core/css/mobile.css
@@ -103,6 +103,10 @@
z-index: 1000;
}
+#app-sidebar{
+ width: 100%;
+}
+
/* allow horizontal scrollbar in settings
otherwise user management is not usable on mobile */
#body-settings #app-content {
diff --git a/core/js/apps.js b/core/js/apps.js
index 71170bbc23a..d0d351f5147 100644
--- a/core/js/apps.js
+++ b/core/js/apps.js
@@ -21,6 +21,26 @@
};
/**
+ * Shows the #app-sidebar and add .with-app-sidebar to subsequent siblings
+ */
+ exports.Apps.showAppSidebar = function() {
+ var $appSidebar = $('#app-sidebar');
+ $appSidebar.removeClass('disappear')
+ $('#app-content').addClass('with-app-sidebar');
+
+ };
+
+ /**
+ * Shows the #app-sidebar and removes .with-app-sidebar from subsequent
+ * siblings
+ */
+ exports.Apps.hideAppSidebar = function() {
+ var $appSidebar = $('#app-sidebar');
+ $appSidebar.addClass('disappear');
+ $('#app-content').removeClass('with-app-sidebar');
+ };
+
+ /**
* Provides a way to slide down a target area through a button and slide it
* up if the user clicks somewhere else. Used for the news app settings and
* add new field.
diff --git a/core/js/core.json b/core/js/core.json
index 0f052b798a9..1053debaa99 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -20,6 +20,7 @@
"oc-dialogs.js",
"js.js",
"l10n.js",
+ "apps.js",
"share.js",
"octemplate.js",
"eventsource.js",
diff --git a/core/js/js.js b/core/js/js.js
index 45c9c90362f..72d4edd28dd 100644
--- a/core/js/js.js
+++ b/core/js/js.js
@@ -1366,13 +1366,13 @@ function initCore() {
// if there is a scrollbar …
if($('#app-content').get(0).scrollHeight > $('#app-content').height()) {
if($(window).width() > 768) {
- controlsWidth = $('#content').width() - $('#app-navigation').width() - getScrollBarWidth();
+ controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width() - getScrollBarWidth();
} else {
controlsWidth = $('#content').width() - getScrollBarWidth();
}
} else { // if there is none
if($(window).width() > 768) {
- controlsWidth = $('#content').width() - $('#app-navigation').width();
+ controlsWidth = $('#content').width() - $('#app-navigation').width() - $('#app-sidebar').width();
} else {
controlsWidth = $('#content').width();
}
diff --git a/core/js/oc-backbone.js b/core/js/oc-backbone.js
new file mode 100644
index 00000000000..75a40979340
--- /dev/null
+++ b/core/js/oc-backbone.js
@@ -0,0 +1,12 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+if(!_.isUndefined(Backbone)) {
+ OC.Backbone = Backbone.noConflict();
+}
diff --git a/core/js/setupchecks.js b/core/js/setupchecks.js
index 35f24b188fa..5a5c12c85e6 100644
--- a/core/js/setupchecks.js
+++ b/core/js/setupchecks.js
@@ -72,6 +72,11 @@
if(data.isUsedTlsLibOutdated) {
messages.push(data.isUsedTlsLibOutdated);
}
+ if(data.phpSupported && data.phpSupported.eol) {
+ messages.push(
+ t('core', 'Your PHP version ({version}) is no longer <a href="{phpLink}">supported by PHP</a>. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by PHP.', {version: data.phpSupported.version, phpLink: 'https://secure.php.net/supported-versions.php'})
+ );
+ }
} else {
messages.push(t('core', 'Error occurred while checking server setup'));
}
diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js
index 29293e89bcb..dbe005ba2e9 100644
--- a/core/js/tests/specHelper.js
+++ b/core/js/tests/specHelper.js
@@ -121,6 +121,8 @@ window.isPhantom = /phantom/i.test(navigator.userAgent);
OC.TestUtil = TestUtil;
}
+ moment.locale('en');
+
// reset plugins
OC.Plugins._plugins = [];
diff --git a/core/js/tests/specs/setupchecksSpec.js b/core/js/tests/specs/setupchecksSpec.js
index ec8a732b4a1..fe12aa4544c 100644
--- a/core/js/tests/specs/setupchecksSpec.js
+++ b/core/js/tests/specs/setupchecksSpec.js
@@ -142,6 +142,23 @@ describe('OC.SetupChecks tests', function() {
done();
});
});
+
+ it('should return an error if the php version is no longer supported', function(done) {
+ var async = OC.SetupChecks.checkSetup();
+
+ suite.server.requests[0].respond(
+ 200,
+ {
+ 'Content-Type': 'application/json',
+ },
+ JSON.stringify({isUrandomAvailable: true, securityDocs: 'https://docs.owncloud.org/myDocs.html', serverHasInternetConnection: true, dataDirectoryProtected: true, isMemcacheConfigured: true, phpSupported: {eol: true, version: '5.4.0'}})
+ );
+
+ async.done(function( data, s, x ){
+ expect(data).toEqual(['Your PHP version (5.4.0) is no longer <a href="https://secure.php.net/supported-versions.php">supported by PHP</a>. We encourage you to upgrade your PHP version to take advantage of performance and security updates provided by PHP.']);
+ done();
+ });
+ });
});
describe('checkGeneric', function() {
diff --git a/core/vendor/.gitignore b/core/vendor/.gitignore
index 81dac0364d0..bcbb59b6f24 100644
--- a/core/vendor/.gitignore
+++ b/core/vendor/.gitignore
@@ -119,3 +119,6 @@ bootstrap/**
!bootstrap/js
bootstrap/js/*
!bootstrap/js/tooltip.js
+
+# backbone
+backbone/backbone-min*
diff --git a/core/vendor/backbone/.bower.json b/core/vendor/backbone/.bower.json
new file mode 100644
index 00000000000..578c8ffb669
--- /dev/null
+++ b/core/vendor/backbone/.bower.json
@@ -0,0 +1,33 @@
+{
+ "name": "backbone",
+ "version": "1.2.1",
+ "main": "backbone.js",
+ "dependencies": {
+ "underscore": ">=1.7.0"
+ },
+ "ignore": [
+ "docs",
+ "examples",
+ "test",
+ "*.yml",
+ "*.html",
+ "*.ico",
+ "*.md",
+ "CNAME",
+ ".*",
+ "karma.*",
+ "component.json",
+ "package.json"
+ ],
+ "homepage": "https://github.com/jashkenas/backbone",
+ "_release": "1.2.1",
+ "_resolution": {
+ "type": "version",
+ "tag": "1.2.1",
+ "commit": "938a8ff934fd4de4f0009f68d43f500f5920b490"
+ },
+ "_source": "git://github.com/jashkenas/backbone.git",
+ "_target": "~1.2.1",
+ "_originalSource": "backbone",
+ "_direct": true
+} \ No newline at end of file
diff --git a/core/vendor/backbone/LICENSE b/core/vendor/backbone/LICENSE
new file mode 100644
index 00000000000..184d1b99645
--- /dev/null
+++ b/core/vendor/backbone/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2010-2015 Jeremy Ashkenas, DocumentCloud
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/core/vendor/backbone/backbone.js b/core/vendor/backbone/backbone.js
new file mode 100644
index 00000000000..58800425c70
--- /dev/null
+++ b/core/vendor/backbone/backbone.js
@@ -0,0 +1,1873 @@
+// Backbone.js 1.2.1
+
+// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+// Backbone may be freely distributed under the MIT license.
+// For all details and documentation:
+// http://backbonejs.org
+
+(function(factory) {
+
+ // Establish the root object, `window` (`self`) in the browser, or `global` on the server.
+ // We use `self` instead of `window` for `WebWorker` support.
+ var root = (typeof self == 'object' && self.self == self && self) ||
+ (typeof global == 'object' && global.global == global && global);
+
+ // Set up Backbone appropriately for the environment. Start with AMD.
+ if (typeof define === 'function' && define.amd) {
+ define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
+ // Export global even in AMD case in case this script is loaded with
+ // others that may still expect a global Backbone.
+ root.Backbone = factory(root, exports, _, $);
+ });
+
+ // Next for Node.js or CommonJS. jQuery may not be needed as a module.
+ } else if (typeof exports !== 'undefined') {
+ var _ = require('underscore'), $;
+ try { $ = require('jquery'); } catch(e) {}
+ factory(root, exports, _, $);
+
+ // Finally, as a browser global.
+ } else {
+ root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
+ }
+
+}(function(root, Backbone, _, $) {
+
+ // Initial Setup
+ // -------------
+
+ // Save the previous value of the `Backbone` variable, so that it can be
+ // restored later on, if `noConflict` is used.
+ var previousBackbone = root.Backbone;
+
+ // Create a local reference to a common array method we'll want to use later.
+ var slice = [].slice;
+
+ // Current version of the library. Keep in sync with `package.json`.
+ Backbone.VERSION = '1.2.1';
+
+ // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
+ // the `$` variable.
+ Backbone.$ = $;
+
+ // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
+ // to its previous owner. Returns a reference to this Backbone object.
+ Backbone.noConflict = function() {
+ root.Backbone = previousBackbone;
+ return this;
+ };
+
+ // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
+ // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
+ // set a `X-Http-Method-Override` header.
+ Backbone.emulateHTTP = false;
+
+ // Turn on `emulateJSON` to support legacy servers that can't deal with direct
+ // `application/json` requests ... this will encode the body as
+ // `application/x-www-form-urlencoded` instead and will send the model in a
+ // form param named `model`.
+ Backbone.emulateJSON = false;
+
+ // Proxy Underscore methods to a Backbone class' prototype using a
+ // particular attribute as the data argument
+ var addMethod = function(length, method, attribute) {
+ switch (length) {
+ case 1: return function() {
+ return _[method](this[attribute]);
+ };
+ case 2: return function(value) {
+ return _[method](this[attribute], value);
+ };
+ case 3: return function(iteratee, context) {
+ return _[method](this[attribute], iteratee, context);
+ };
+ case 4: return function(iteratee, defaultVal, context) {
+ return _[method](this[attribute], iteratee, defaultVal, context);
+ };
+ default: return function() {
+ var args = slice.call(arguments);
+ args.unshift(this[attribute]);
+ return _[method].apply(_, args);
+ };
+ }
+ };
+ var addUnderscoreMethods = function(Class, methods, attribute) {
+ _.each(methods, function(length, method) {
+ if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
+ });
+ };
+
+ // Backbone.Events
+ // ---------------
+
+ // A module that can be mixed in to *any object* in order to provide it with
+ // custom events. You may bind with `on` or remove with `off` callback
+ // functions to an event; `trigger`-ing an event fires all callbacks in
+ // succession.
+ //
+ // var object = {};
+ // _.extend(object, Backbone.Events);
+ // object.on('expand', function(){ alert('expanded'); });
+ // object.trigger('expand');
+ //
+ var Events = Backbone.Events = {};
+
+ // Regular expression used to split event strings.
+ var eventSplitter = /\s+/;
+
+ // Iterates over the standard `event, callback` (as well as the fancy multiple
+ // space-separated events `"change blur", callback` and jQuery-style event
+ // maps `{event: callback}`), reducing them by manipulating `memo`.
+ // Passes a normalized single event name and callback, as well as any
+ // optional `opts`.
+ var eventsApi = function(iteratee, memo, name, callback, opts) {
+ var i = 0, names;
+ if (name && typeof name === 'object') {
+ // Handle event maps.
+ if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
+ for (names = _.keys(name); i < names.length ; i++) {
+ memo = iteratee(memo, names[i], name[names[i]], opts);
+ }
+ } else if (name && eventSplitter.test(name)) {
+ // Handle space separated event names.
+ for (names = name.split(eventSplitter); i < names.length; i++) {
+ memo = iteratee(memo, names[i], callback, opts);
+ }
+ } else {
+ memo = iteratee(memo, name, callback, opts);
+ }
+ return memo;
+ };
+
+ // Bind an event to a `callback` function. Passing `"all"` will bind
+ // the callback to all events fired.
+ Events.on = function(name, callback, context) {
+ return internalOn(this, name, callback, context);
+ };
+
+ // An internal use `on` function, used to guard the `listening` argument from
+ // the public API.
+ var internalOn = function(obj, name, callback, context, listening) {
+ obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
+ context: context,
+ ctx: obj,
+ listening: listening
+ });
+
+ if (listening) {
+ var listeners = obj._listeners || (obj._listeners = {});
+ listeners[listening.id] = listening;
+ }
+
+ return obj;
+ };
+
+ // Inversion-of-control versions of `on`. Tell *this* object to listen to
+ // an event in another object... keeping track of what it's listening to.
+ Events.listenTo = function(obj, name, callback) {
+ if (!obj) return this;
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
+ var listening = listeningTo[id];
+
+ // This object is not listening to any other events on `obj` yet.
+ // Setup the necessary references to track the listening callbacks.
+ if (!listening) {
+ var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
+ listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
+ }
+
+ // Bind callbacks on obj, and keep track of them on listening.
+ internalOn(obj, name, callback, this, listening);
+ return this;
+ };
+
+ // The reducing API that adds a callback to the `events` object.
+ var onApi = function(events, name, callback, options) {
+ if (callback) {
+ var handlers = events[name] || (events[name] = []);
+ var context = options.context, ctx = options.ctx, listening = options.listening;
+ if (listening) listening.count++;
+
+ handlers.push({ callback: callback, context: context, ctx: context || ctx, listening: listening });
+ }
+ return events;
+ };
+
+ // Remove one or many callbacks. If `context` is null, removes all
+ // callbacks with that function. If `callback` is null, removes all
+ // callbacks for the event. If `name` is null, removes all bound
+ // callbacks for all events.
+ Events.off = function(name, callback, context) {
+ if (!this._events) return this;
+ this._events = eventsApi(offApi, this._events, name, callback, {
+ context: context,
+ listeners: this._listeners
+ });
+ return this;
+ };
+
+ // Tell this object to stop listening to either specific events ... or
+ // to every object it's currently listening to.
+ Events.stopListening = function(obj, name, callback) {
+ var listeningTo = this._listeningTo;
+ if (!listeningTo) return this;
+
+ var ids = obj ? [obj._listenId] : _.keys(listeningTo);
+
+ for (var i = 0; i < ids.length; i++) {
+ var listening = listeningTo[ids[i]];
+
+ // If listening doesn't exist, this object is not currently
+ // listening to obj. Break out early.
+ if (!listening) break;
+
+ listening.obj.off(name, callback, this);
+ }
+ if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
+
+ return this;
+ };
+
+ // The reducing API that removes a callback from the `events` object.
+ var offApi = function(events, name, callback, options) {
+ // No events to consider.
+ if (!events) return;
+
+ var i = 0, listening;
+ var context = options.context, listeners = options.listeners;
+
+ // Delete all events listeners and "drop" events.
+ if (!name && !callback && !context) {
+ var ids = _.keys(listeners);
+ for (; i < ids.length; i++) {
+ listening = listeners[ids[i]];
+ delete listeners[listening.id];
+ delete listening.listeningTo[listening.objId];
+ }
+ return;
+ }
+
+ var names = name ? [name] : _.keys(events);
+ for (; i < names.length; i++) {
+ name = names[i];
+ var handlers = events[name];
+
+ // Bail out if there are no events stored.
+ if (!handlers) break;
+
+ // Replace events if there are any remaining. Otherwise, clean up.
+ var remaining = [];
+ for (var j = 0; j < handlers.length; j++) {
+ var handler = handlers[j];
+ if (
+ callback && callback !== handler.callback &&
+ callback !== handler.callback._callback ||
+ context && context !== handler.context
+ ) {
+ remaining.push(handler);
+ } else {
+ listening = handler.listening;
+ if (listening && --listening.count === 0) {
+ delete listeners[listening.id];
+ delete listening.listeningTo[listening.objId];
+ }
+ }
+ }
+
+ // Update tail event if the list has any events. Otherwise, clean up.
+ if (remaining.length) {
+ events[name] = remaining;
+ } else {
+ delete events[name];
+ }
+ }
+ if (_.size(events)) return events;
+ };
+
+ // Bind an event to only be triggered a single time. After the first time
+ // the callback is invoked, it will be removed. When multiple events are
+ // passed in using the space-separated syntax, the event will fire once for every
+ // event you passed in, not once for a combination of all events
+ Events.once = function(name, callback, context) {
+ // Map the event into a `{event: once}` object.
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
+ return this.on(events, void 0, context);
+ };
+
+ // Inversion-of-control versions of `once`.
+ Events.listenToOnce = function(obj, name, callback) {
+ // Map the event into a `{event: once}` object.
+ var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
+ return this.listenTo(obj, events);
+ };
+
+ // Reduces the event callbacks into a map of `{event: onceWrapper}`.
+ // `offer` unbinds the `onceWrapper` after it has been called.
+ var onceMap = function(map, name, callback, offer) {
+ if (callback) {
+ var once = map[name] = _.once(function() {
+ offer(name, once);
+ callback.apply(this, arguments);
+ });
+ once._callback = callback;
+ }
+ return map;
+ };
+
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
+ // passed the same arguments as `trigger` is, apart from the event name
+ // (unless you're listening on `"all"`, which will cause your callback to
+ // receive the true name of the event as the first argument).
+ Events.trigger = function(name) {
+ if (!this._events) return this;
+
+ var length = Math.max(0, arguments.length - 1);
+ var args = Array(length);
+ for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
+
+ eventsApi(triggerApi, this._events, name, void 0, args);
+ return this;
+ };
+
+ // Handles triggering the appropriate event callbacks.
+ var triggerApi = function(objEvents, name, cb, args) {
+ if (objEvents) {
+ var events = objEvents[name];
+ var allEvents = objEvents.all;
+ if (events && allEvents) allEvents = allEvents.slice();
+ if (events) triggerEvents(events, args);
+ if (allEvents) triggerEvents(allEvents, [name].concat(args));
+ }
+ return objEvents;
+ };
+
+ // A difficult-to-believe, but optimized internal dispatch function for
+ // triggering events. Tries to keep the usual cases speedy (most internal
+ // Backbone events have 3 arguments).
+ var triggerEvents = function(events, args) {
+ var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
+ switch (args.length) {
+ case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
+ case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
+ case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
+ case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
+ default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
+ }
+ };
+
+ // Aliases for backwards compatibility.
+ Events.bind = Events.on;
+ Events.unbind = Events.off;
+
+ // Allow the `Backbone` object to serve as a global event bus, for folks who
+ // want global "pubsub" in a convenient place.
+ _.extend(Backbone, Events);
+
+ // Backbone.Model
+ // --------------
+
+ // Backbone **Models** are the basic data object in the framework --
+ // frequently representing a row in a table in a database on your server.
+ // A discrete chunk of data and a bunch of useful, related methods for
+ // performing computations and transformations on that data.
+
+ // Create a new model with the specified attributes. A client id (`cid`)
+ // is automatically generated and assigned for you.
+ var Model = Backbone.Model = function(attributes, options) {
+ var attrs = attributes || {};
+ options || (options = {});
+ this.cid = _.uniqueId(this.cidPrefix);
+ this.attributes = {};
+ if (options.collection) this.collection = options.collection;
+ if (options.parse) attrs = this.parse(attrs, options) || {};
+ attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
+ this.set(attrs, options);
+ this.changed = {};
+ this.initialize.apply(this, arguments);
+ };
+
+ // Attach all inheritable methods to the Model prototype.
+ _.extend(Model.prototype, Events, {
+
+ // A hash of attributes whose current and previous value differ.
+ changed: null,
+
+ // The value returned during the last failed validation.
+ validationError: null,
+
+ // The default name for the JSON `id` attribute is `"id"`. MongoDB and
+ // CouchDB users may want to set this to `"_id"`.
+ idAttribute: 'id',
+
+ // The prefix is used to create the client id which is used to identify models locally.
+ // You may want to override this if you're experiencing name clashes with model ids.
+ cidPrefix: 'c',
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // Return a copy of the model's `attributes` object.
+ toJSON: function(options) {
+ return _.clone(this.attributes);
+ },
+
+ // Proxy `Backbone.sync` by default -- but override this if you need
+ // custom syncing semantics for *this* particular model.
+ sync: function() {
+ return Backbone.sync.apply(this, arguments);
+ },
+
+ // Get the value of an attribute.
+ get: function(attr) {
+ return this.attributes[attr];
+ },
+
+ // Get the HTML-escaped value of an attribute.
+ escape: function(attr) {
+ return _.escape(this.get(attr));
+ },
+
+ // Returns `true` if the attribute contains a value that is not null
+ // or undefined.
+ has: function(attr) {
+ return this.get(attr) != null;
+ },
+
+ // Special-cased proxy to underscore's `_.matches` method.
+ matches: function(attrs) {
+ return !!_.iteratee(attrs, this)(this.attributes);
+ },
+
+ // Set a hash of model attributes on the object, firing `"change"`. This is
+ // the core primitive operation of a model, updating the data and notifying
+ // anyone who needs to know about the change in state. The heart of the beast.
+ set: function(key, val, options) {
+ if (key == null) return this;
+
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ var attrs;
+ if (typeof key === 'object') {
+ attrs = key;
+ options = val;
+ } else {
+ (attrs = {})[key] = val;
+ }
+
+ options || (options = {});
+
+ // Run validation.
+ if (!this._validate(attrs, options)) return false;
+
+ // Extract attributes and options.
+ var unset = options.unset;
+ var silent = options.silent;
+ var changes = [];
+ var changing = this._changing;
+ this._changing = true;
+
+ if (!changing) {
+ this._previousAttributes = _.clone(this.attributes);
+ this.changed = {};
+ }
+
+ var current = this.attributes;
+ var changed = this.changed;
+ var prev = this._previousAttributes;
+
+ // Check for changes of `id`.
+ if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];
+
+ // For each `set` attribute, update or delete the current value.
+ for (var attr in attrs) {
+ val = attrs[attr];
+ if (!_.isEqual(current[attr], val)) changes.push(attr);
+ if (!_.isEqual(prev[attr], val)) {
+ changed[attr] = val;
+ } else {
+ delete changed[attr];
+ }
+ unset ? delete current[attr] : current[attr] = val;
+ }
+
+ // Trigger all relevant attribute changes.
+ if (!silent) {
+ if (changes.length) this._pending = options;
+ for (var i = 0; i < changes.length; i++) {
+ this.trigger('change:' + changes[i], this, current[changes[i]], options);
+ }
+ }
+
+ // You might be wondering why there's a `while` loop here. Changes can
+ // be recursively nested within `"change"` events.
+ if (changing) return this;
+ if (!silent) {
+ while (this._pending) {
+ options = this._pending;
+ this._pending = false;
+ this.trigger('change', this, options);
+ }
+ }
+ this._pending = false;
+ this._changing = false;
+ return this;
+ },
+
+ // Remove an attribute from the model, firing `"change"`. `unset` is a noop
+ // if the attribute doesn't exist.
+ unset: function(attr, options) {
+ return this.set(attr, void 0, _.extend({}, options, {unset: true}));
+ },
+
+ // Clear all attributes on the model, firing `"change"`.
+ clear: function(options) {
+ var attrs = {};
+ for (var key in this.attributes) attrs[key] = void 0;
+ return this.set(attrs, _.extend({}, options, {unset: true}));
+ },
+
+ // Determine if the model has changed since the last `"change"` event.
+ // If you specify an attribute name, determine if that attribute has changed.
+ hasChanged: function(attr) {
+ if (attr == null) return !_.isEmpty(this.changed);
+ return _.has(this.changed, attr);
+ },
+
+ // Return an object containing all the attributes that have changed, or
+ // false if there are no changed attributes. Useful for determining what
+ // parts of a view need to be updated and/or what attributes need to be
+ // persisted to the server. Unset attributes will be set to undefined.
+ // You can also pass an attributes object to diff against the model,
+ // determining if there *would be* a change.
+ changedAttributes: function(diff) {
+ if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
+ var old = this._changing ? this._previousAttributes : this.attributes;
+ var changed = {};
+ for (var attr in diff) {
+ var val = diff[attr];
+ if (_.isEqual(old[attr], val)) continue;
+ changed[attr] = val;
+ }
+ return _.size(changed) ? changed : false;
+ },
+
+ // Get the previous value of an attribute, recorded at the time the last
+ // `"change"` event was fired.
+ previous: function(attr) {
+ if (attr == null || !this._previousAttributes) return null;
+ return this._previousAttributes[attr];
+ },
+
+ // Get all of the attributes of the model at the time of the previous
+ // `"change"` event.
+ previousAttributes: function() {
+ return _.clone(this._previousAttributes);
+ },
+
+ // Fetch the model from the server, merging the response with the model's
+ // local attributes. Any changed attributes will trigger a "change" event.
+ fetch: function(options) {
+ options = _.extend({parse: true}, options);
+ var model = this;
+ var success = options.success;
+ options.success = function(resp) {
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
+ if (!model.set(serverAttrs, options)) return false;
+ if (success) success.call(options.context, model, resp, options);
+ model.trigger('sync', model, resp, options);
+ };
+ wrapError(this, options);
+ return this.sync('read', this, options);
+ },
+
+ // Set a hash of model attributes, and sync the model to the server.
+ // If the server returns an attributes hash that differs, the model's
+ // state will be `set` again.
+ save: function(key, val, options) {
+ // Handle both `"key", value` and `{key: value}` -style arguments.
+ var attrs;
+ if (key == null || typeof key === 'object') {
+ attrs = key;
+ options = val;
+ } else {
+ (attrs = {})[key] = val;
+ }
+
+ options = _.extend({validate: true, parse: true}, options);
+ var wait = options.wait;
+
+ // If we're not waiting and attributes exist, save acts as
+ // `set(attr).save(null, opts)` with validation. Otherwise, check if
+ // the model will be valid when the attributes, if any, are set.
+ if (attrs && !wait) {
+ if (!this.set(attrs, options)) return false;
+ } else {
+ if (!this._validate(attrs, options)) return false;
+ }
+
+ // After a successful server-side save, the client is (optionally)
+ // updated with the server-side state.
+ var model = this;
+ var success = options.success;
+ var attributes = this.attributes;
+ options.success = function(resp) {
+ // Ensure attributes are restored during synchronous saves.
+ model.attributes = attributes;
+ var serverAttrs = options.parse ? model.parse(resp, options) : resp;
+ if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
+ if (serverAttrs && !model.set(serverAttrs, options)) return false;
+ if (success) success.call(options.context, model, resp, options);
+ model.trigger('sync', model, resp, options);
+ };
+ wrapError(this, options);
+
+ // Set temporary attributes if `{wait: true}` to properly find new ids.
+ if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
+
+ var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
+ if (method === 'patch' && !options.attrs) options.attrs = attrs;
+ var xhr = this.sync(method, this, options);
+
+ // Restore attributes.
+ this.attributes = attributes;
+
+ return xhr;
+ },
+
+ // Destroy this model on the server if it was already persisted.
+ // Optimistically removes the model from its collection, if it has one.
+ // If `wait: true` is passed, waits for the server to respond before removal.
+ destroy: function(options) {
+ options = options ? _.clone(options) : {};
+ var model = this;
+ var success = options.success;
+ var wait = options.wait;
+
+ var destroy = function() {
+ model.stopListening();
+ model.trigger('destroy', model, model.collection, options);
+ };
+
+ options.success = function(resp) {
+ if (wait) destroy();
+ if (success) success.call(options.context, model, resp, options);
+ if (!model.isNew()) model.trigger('sync', model, resp, options);
+ };
+
+ var xhr = false;
+ if (this.isNew()) {
+ _.defer(options.success);
+ } else {
+ wrapError(this, options);
+ xhr = this.sync('delete', this, options);
+ }
+ if (!wait) destroy();
+ return xhr;
+ },
+
+ // Default URL for the model's representation on the server -- if you're
+ // using Backbone's restful methods, override this to change the endpoint
+ // that will be called.
+ url: function() {
+ var base =
+ _.result(this, 'urlRoot') ||
+ _.result(this.collection, 'url') ||
+ urlError();
+ if (this.isNew()) return base;
+ var id = this.get(this.idAttribute);
+ return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
+ },
+
+ // **parse** converts a response into the hash of attributes to be `set` on
+ // the model. The default implementation is just to pass the response along.
+ parse: function(resp, options) {
+ return resp;
+ },
+
+ // Create a new model with identical attributes to this one.
+ clone: function() {
+ return new this.constructor(this.attributes);
+ },
+
+ // A model is new if it has never been saved to the server, and lacks an id.
+ isNew: function() {
+ return !this.has(this.idAttribute);
+ },
+
+ // Check if the model is currently in a valid state.
+ isValid: function(options) {
+ return this._validate({}, _.defaults({validate: true}, options));
+ },
+
+ // Run validation against the next complete set of model attributes,
+ // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
+ _validate: function(attrs, options) {
+ if (!options.validate || !this.validate) return true;
+ attrs = _.extend({}, this.attributes, attrs);
+ var error = this.validationError = this.validate(attrs, options) || null;
+ if (!error) return true;
+ this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
+ return false;
+ }
+
+ });
+
+ // Underscore methods that we want to implement on the Model.
+ var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
+ omit: 0, chain: 1, isEmpty: 1 };
+
+ // Mix in each Underscore method as a proxy to `Model#attributes`.
+ addUnderscoreMethods(Model, modelMethods, 'attributes');
+
+ // Backbone.Collection
+ // -------------------
+
+ // If models tend to represent a single row of data, a Backbone Collection is
+ // more analogous to a table full of data ... or a small slice or page of that
+ // table, or a collection of rows that belong together for a particular reason
+ // -- all of the messages in this particular folder, all of the documents
+ // belonging to this particular author, and so on. Collections maintain
+ // indexes of their models, both in order, and for lookup by `id`.
+
+ // Create a new **Collection**, perhaps to contain a specific type of `model`.
+ // If a `comparator` is specified, the Collection will maintain
+ // its models in sort order, as they're added and removed.
+ var Collection = Backbone.Collection = function(models, options) {
+ options || (options = {});
+ if (options.model) this.model = options.model;
+ if (options.comparator !== void 0) this.comparator = options.comparator;
+ this._reset();
+ this.initialize.apply(this, arguments);
+ if (models) this.reset(models, _.extend({silent: true}, options));
+ };
+
+ // Default options for `Collection#set`.
+ var setOptions = {add: true, remove: true, merge: true};
+ var addOptions = {add: true, remove: false};
+
+ // Define the Collection's inheritable methods.
+ _.extend(Collection.prototype, Events, {
+
+ // The default model for a collection is just a **Backbone.Model**.
+ // This should be overridden in most cases.
+ model: Model,
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // The JSON representation of a Collection is an array of the
+ // models' attributes.
+ toJSON: function(options) {
+ return this.map(function(model) { return model.toJSON(options); });
+ },
+
+ // Proxy `Backbone.sync` by default.
+ sync: function() {
+ return Backbone.sync.apply(this, arguments);
+ },
+
+ // Add a model, or list of models to the set.
+ add: function(models, options) {
+ return this.set(models, _.extend({merge: false}, options, addOptions));
+ },
+
+ // Remove a model, or a list of models from the set.
+ remove: function(models, options) {
+ options = _.extend({}, options);
+ var singular = !_.isArray(models);
+ models = singular ? [models] : _.clone(models);
+ var removed = this._removeModels(models, options);
+ if (!options.silent && removed) this.trigger('update', this, options);
+ return singular ? removed[0] : removed;
+ },
+
+ // Update a collection by `set`-ing a new list of models, adding new ones,
+ // removing models that are no longer present, and merging models that
+ // already exist in the collection, as necessary. Similar to **Model#set**,
+ // the core operation for updating the data contained by the collection.
+ set: function(models, options) {
+ options = _.defaults({}, options, setOptions);
+ if (options.parse && !this._isModel(models)) models = this.parse(models, options);
+ var singular = !_.isArray(models);
+ models = singular ? (models ? [models] : []) : models.slice();
+ var id, model, attrs, existing, sort;
+ var at = options.at;
+ if (at != null) at = +at;
+ if (at < 0) at += this.length + 1;
+ var sortable = this.comparator && (at == null) && options.sort !== false;
+ var sortAttr = _.isString(this.comparator) ? this.comparator : null;
+ var toAdd = [], toRemove = [], modelMap = {};
+ var add = options.add, merge = options.merge, remove = options.remove;
+ var order = !sortable && add && remove ? [] : false;
+ var orderChanged = false;
+
+ // Turn bare objects into model references, and prevent invalid models
+ // from being added.
+ for (var i = 0; i < models.length; i++) {
+ attrs = models[i];
+
+ // If a duplicate is found, prevent it from being added and
+ // optionally merge it into the existing model.
+ if (existing = this.get(attrs)) {
+ if (remove) modelMap[existing.cid] = true;
+ if (merge && attrs !== existing) {
+ attrs = this._isModel(attrs) ? attrs.attributes : attrs;
+ if (options.parse) attrs = existing.parse(attrs, options);
+ existing.set(attrs, options);
+ if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true;
+ }
+ models[i] = existing;
+
+ // If this is a new, valid model, push it to the `toAdd` list.
+ } else if (add) {
+ model = models[i] = this._prepareModel(attrs, options);
+ if (!model) continue;
+ toAdd.push(model);
+ this._addReference(model, options);
+ }
+
+ // Do not add multiple models with the same `id`.
+ model = existing || model;
+ if (!model) continue;
+ id = this.modelId(model.attributes);
+ if (order && (model.isNew() || !modelMap[id])) {
+ order.push(model);
+
+ // Check to see if this is actually a new model at this index.
+ orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid;
+ }
+
+ modelMap[id] = true;
+ }
+
+ // Remove nonexistent models if appropriate.
+ if (remove) {
+ for (var i = 0; i < this.length; i++) {
+ if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
+ }
+ if (toRemove.length) this._removeModels(toRemove, options);
+ }
+
+ // See if sorting is needed, update `length` and splice in new models.
+ if (toAdd.length || orderChanged) {
+ if (sortable) sort = true;
+ this.length += toAdd.length;
+ if (at != null) {
+ for (var i = 0; i < toAdd.length; i++) {
+ this.models.splice(at + i, 0, toAdd[i]);
+ }
+ } else {
+ if (order) this.models.length = 0;
+ var orderedModels = order || toAdd;
+ for (var i = 0; i < orderedModels.length; i++) {
+ this.models.push(orderedModels[i]);
+ }
+ }
+ }
+
+ // Silently sort the collection if appropriate.
+ if (sort) this.sort({silent: true});
+
+ // Unless silenced, it's time to fire all appropriate add/sort events.
+ if (!options.silent) {
+ var addOpts = at != null ? _.clone(options) : options;
+ for (var i = 0; i < toAdd.length; i++) {
+ if (at != null) addOpts.index = at + i;
+ (model = toAdd[i]).trigger('add', model, this, addOpts);
+ }
+ if (sort || orderChanged) this.trigger('sort', this, options);
+ if (toAdd.length || toRemove.length) this.trigger('update', this, options);
+ }
+
+ // Return the added (or merged) model (or models).
+ return singular ? models[0] : models;
+ },
+
+ // When you have more items than you want to add or remove individually,
+ // you can reset the entire set with a new list of models, without firing
+ // any granular `add` or `remove` events. Fires `reset` when finished.
+ // Useful for bulk operations and optimizations.
+ reset: function(models, options) {
+ options = options ? _.clone(options) : {};
+ for (var i = 0; i < this.models.length; i++) {
+ this._removeReference(this.models[i], options);
+ }
+ options.previousModels = this.models;
+ this._reset();
+ models = this.add(models, _.extend({silent: true}, options));
+ if (!options.silent) this.trigger('reset', this, options);
+ return models;
+ },
+
+ // Add a model to the end of the collection.
+ push: function(model, options) {
+ return this.add(model, _.extend({at: this.length}, options));
+ },
+
+ // Remove a model from the end of the collection.
+ pop: function(options) {
+ var model = this.at(this.length - 1);
+ return this.remove(model, options);
+ },
+
+ // Add a model to the beginning of the collection.
+ unshift: function(model, options) {
+ return this.add(model, _.extend({at: 0}, options));
+ },
+
+ // Remove a model from the beginning of the collection.
+ shift: function(options) {
+ var model = this.at(0);
+ return this.remove(model, options);
+ },
+
+ // Slice out a sub-array of models from the collection.
+ slice: function() {
+ return slice.apply(this.models, arguments);
+ },
+
+ // Get a model from the set by id.
+ get: function(obj) {
+ if (obj == null) return void 0;
+ var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
+ return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
+ },
+
+ // Get the model at the given index.
+ at: function(index) {
+ if (index < 0) index += this.length;
+ return this.models[index];
+ },
+
+ // Return models with matching attributes. Useful for simple cases of
+ // `filter`.
+ where: function(attrs, first) {
+ var matches = _.matches(attrs);
+ return this[first ? 'find' : 'filter'](function(model) {
+ return matches(model.attributes);
+ });
+ },
+
+ // Return the first model with matching attributes. Useful for simple cases
+ // of `find`.
+ findWhere: function(attrs) {
+ return this.where(attrs, true);
+ },
+
+ // Force the collection to re-sort itself. You don't need to call this under
+ // normal circumstances, as the set will maintain sort order as each item
+ // is added.
+ sort: function(options) {
+ if (!this.comparator) throw new Error('Cannot sort a set without a comparator');
+ options || (options = {});
+
+ // Run sort based on type of `comparator`.
+ if (_.isString(this.comparator) || this.comparator.length === 1) {
+ this.models = this.sortBy(this.comparator, this);
+ } else {
+ this.models.sort(_.bind(this.comparator, this));
+ }
+
+ if (!options.silent) this.trigger('sort', this, options);
+ return this;
+ },
+
+ // Pluck an attribute from each model in the collection.
+ pluck: function(attr) {
+ return _.invoke(this.models, 'get', attr);
+ },
+
+ // Fetch the default set of models for this collection, resetting the
+ // collection when they arrive. If `reset: true` is passed, the response
+ // data will be passed through the `reset` method instead of `set`.
+ fetch: function(options) {
+ options = _.extend({parse: true}, options);
+ var success = options.success;
+ var collection = this;
+ options.success = function(resp) {
+ var method = options.reset ? 'reset' : 'set';
+ collection[method](resp, options);
+ if (success) success.call(options.context, collection, resp, options);
+ collection.trigger('sync', collection, resp, options);
+ };
+ wrapError(this, options);
+ return this.sync('read', this, options);
+ },
+
+ // Create a new instance of a model in this collection. Add the model to the
+ // collection immediately, unless `wait: true` is passed, in which case we
+ // wait for the server to agree.
+ create: function(model, options) {
+ options = options ? _.clone(options) : {};
+ var wait = options.wait;
+ model = this._prepareModel(model, options);
+ if (!model) return false;
+ if (!wait) this.add(model, options);
+ var collection = this;
+ var success = options.success;
+ options.success = function(model, resp, callbackOpts) {
+ if (wait) collection.add(model, callbackOpts);
+ if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
+ };
+ model.save(null, options);
+ return model;
+ },
+
+ // **parse** converts a response into a list of models to be added to the
+ // collection. The default implementation is just to pass it through.
+ parse: function(resp, options) {
+ return resp;
+ },
+
+ // Create a new collection with an identical list of models as this one.
+ clone: function() {
+ return new this.constructor(this.models, {
+ model: this.model,
+ comparator: this.comparator
+ });
+ },
+
+ // Define how to uniquely identify models in the collection.
+ modelId: function (attrs) {
+ return attrs[this.model.prototype.idAttribute || 'id'];
+ },
+
+ // Private method to reset all internal state. Called when the collection
+ // is first initialized or reset.
+ _reset: function() {
+ this.length = 0;
+ this.models = [];
+ this._byId = {};
+ },
+
+ // Prepare a hash of attributes (or other model) to be added to this
+ // collection.
+ _prepareModel: function(attrs, options) {
+ if (this._isModel(attrs)) {
+ if (!attrs.collection) attrs.collection = this;
+ return attrs;
+ }
+ options = options ? _.clone(options) : {};
+ options.collection = this;
+ var model = new this.model(attrs, options);
+ if (!model.validationError) return model;
+ this.trigger('invalid', this, model.validationError, options);
+ return false;
+ },
+
+ // Internal method called by both remove and set.
+ // Returns removed models, or false if nothing is removed.
+ _removeModels: function(models, options) {
+ var removed = [];
+ for (var i = 0; i < models.length; i++) {
+ var model = this.get(models[i]);
+ if (!model) continue;
+
+ var index = this.indexOf(model);
+ this.models.splice(index, 1);
+ this.length--;
+
+ if (!options.silent) {
+ options.index = index;
+ model.trigger('remove', model, this, options);
+ }
+
+ removed.push(model);
+ this._removeReference(model, options);
+ }
+ return removed.length ? removed : false;
+ },
+
+ // Method for checking whether an object should be considered a model for
+ // the purposes of adding to the collection.
+ _isModel: function (model) {
+ return model instanceof Model;
+ },
+
+ // Internal method to create a model's ties to a collection.
+ _addReference: function(model, options) {
+ this._byId[model.cid] = model;
+ var id = this.modelId(model.attributes);
+ if (id != null) this._byId[id] = model;
+ model.on('all', this._onModelEvent, this);
+ },
+
+ // Internal method to sever a model's ties to a collection.
+ _removeReference: function(model, options) {
+ delete this._byId[model.cid];
+ var id = this.modelId(model.attributes);
+ if (id != null) delete this._byId[id];
+ if (this === model.collection) delete model.collection;
+ model.off('all', this._onModelEvent, this);
+ },
+
+ // Internal method called every time a model in the set fires an event.
+ // Sets need to update their indexes when models change ids. All other
+ // events simply proxy through. "add" and "remove" events that originate
+ // in other collections are ignored.
+ _onModelEvent: function(event, model, collection, options) {
+ if ((event === 'add' || event === 'remove') && collection !== this) return;
+ if (event === 'destroy') this.remove(model, options);
+ if (event === 'change') {
+ var prevId = this.modelId(model.previousAttributes());
+ var id = this.modelId(model.attributes);
+ if (prevId !== id) {
+ if (prevId != null) delete this._byId[prevId];
+ if (id != null) this._byId[id] = model;
+ }
+ }
+ this.trigger.apply(this, arguments);
+ }
+
+ });
+
+ // Underscore methods that we want to implement on the Collection.
+ // 90% of the core usefulness of Backbone Collections is actually implemented
+ // right here:
+ var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
+ foldl: 4, inject: 4, reduceRight: 4, foldr: 4, find: 3, detect: 3, filter: 3,
+ select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 2,
+ contains: 2, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
+ head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
+ without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
+ isEmpty: 1, chain: 1, sample: 3, partition: 3 };
+
+ // Mix in each Underscore method as a proxy to `Collection#models`.
+ addUnderscoreMethods(Collection, collectionMethods, 'models');
+
+ // Underscore methods that take a property name as an argument.
+ var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy'];
+
+ // Use attributes instead of properties.
+ _.each(attributeMethods, function(method) {
+ if (!_[method]) return;
+ Collection.prototype[method] = function(value, context) {
+ var iterator = _.isFunction(value) ? value : function(model) {
+ return model.get(value);
+ };
+ return _[method](this.models, iterator, context);
+ };
+ });
+
+ // Backbone.View
+ // -------------
+
+ // Backbone Views are almost more convention than they are actual code. A View
+ // is simply a JavaScript object that represents a logical chunk of UI in the
+ // DOM. This might be a single item, an entire list, a sidebar or panel, or
+ // even the surrounding frame which wraps your whole app. Defining a chunk of
+ // UI as a **View** allows you to define your DOM events declaratively, without
+ // having to worry about render order ... and makes it easy for the view to
+ // react to specific changes in the state of your models.
+
+ // Creating a Backbone.View creates its initial element outside of the DOM,
+ // if an existing element is not provided...
+ var View = Backbone.View = function(options) {
+ this.cid = _.uniqueId('view');
+ _.extend(this, _.pick(options, viewOptions));
+ this._ensureElement();
+ this.initialize.apply(this, arguments);
+ };
+
+ // Cached regex to split keys for `delegate`.
+ var delegateEventSplitter = /^(\S+)\s*(.*)$/;
+
+ // List of view options to be merged as properties.
+ var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
+
+ // Set up all inheritable **Backbone.View** properties and methods.
+ _.extend(View.prototype, Events, {
+
+ // The default `tagName` of a View's element is `"div"`.
+ tagName: 'div',
+
+ // jQuery delegate for element lookup, scoped to DOM elements within the
+ // current view. This should be preferred to global lookups where possible.
+ $: function(selector) {
+ return this.$el.find(selector);
+ },
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // **render** is the core function that your view should override, in order
+ // to populate its element (`this.el`), with the appropriate HTML. The
+ // convention is for **render** to always return `this`.
+ render: function() {
+ return this;
+ },
+
+ // Remove this view by taking the element out of the DOM, and removing any
+ // applicable Backbone.Events listeners.
+ remove: function() {
+ this._removeElement();
+ this.stopListening();
+ return this;
+ },
+
+ // Remove this view's element from the document and all event listeners
+ // attached to it. Exposed for subclasses using an alternative DOM
+ // manipulation API.
+ _removeElement: function() {
+ this.$el.remove();
+ },
+
+ // Change the view's element (`this.el` property) and re-delegate the
+ // view's events on the new element.
+ setElement: function(element) {
+ this.undelegateEvents();
+ this._setElement(element);
+ this.delegateEvents();
+ return this;
+ },
+
+ // Creates the `this.el` and `this.$el` references for this view using the
+ // given `el`. `el` can be a CSS selector or an HTML string, a jQuery
+ // context or an element. Subclasses can override this to utilize an
+ // alternative DOM manipulation API and are only required to set the
+ // `this.el` property.
+ _setElement: function(el) {
+ this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
+ this.el = this.$el[0];
+ },
+
+ // Set callbacks, where `this.events` is a hash of
+ //
+ // *{"event selector": "callback"}*
+ //
+ // {
+ // 'mousedown .title': 'edit',
+ // 'click .button': 'save',
+ // 'click .open': function(e) { ... }
+ // }
+ //
+ // pairs. Callbacks will be bound to the view, with `this` set properly.
+ // Uses event delegation for efficiency.
+ // Omitting the selector binds the event to `this.el`.
+ delegateEvents: function(events) {
+ events || (events = _.result(this, 'events'));
+ if (!events) return this;
+ this.undelegateEvents();
+ for (var key in events) {
+ var method = events[key];
+ if (!_.isFunction(method)) method = this[method];
+ if (!method) continue;
+ var match = key.match(delegateEventSplitter);
+ this.delegate(match[1], match[2], _.bind(method, this));
+ }
+ return this;
+ },
+
+ // Add a single event listener to the view's element (or a child element
+ // using `selector`). This only works for delegate-able events: not `focus`,
+ // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
+ delegate: function(eventName, selector, listener) {
+ this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
+ return this;
+ },
+
+ // Clears all callbacks previously bound to the view by `delegateEvents`.
+ // You usually don't need to use this, but may wish to if you have multiple
+ // Backbone views attached to the same DOM element.
+ undelegateEvents: function() {
+ if (this.$el) this.$el.off('.delegateEvents' + this.cid);
+ return this;
+ },
+
+ // A finer-grained `undelegateEvents` for removing a single delegated event.
+ // `selector` and `listener` are both optional.
+ undelegate: function(eventName, selector, listener) {
+ this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
+ return this;
+ },
+
+ // Produces a DOM element to be assigned to your view. Exposed for
+ // subclasses using an alternative DOM manipulation API.
+ _createElement: function(tagName) {
+ return document.createElement(tagName);
+ },
+
+ // Ensure that the View has a DOM element to render into.
+ // If `this.el` is a string, pass it through `$()`, take the first
+ // matching element, and re-assign it to `el`. Otherwise, create
+ // an element from the `id`, `className` and `tagName` properties.
+ _ensureElement: function() {
+ if (!this.el) {
+ var attrs = _.extend({}, _.result(this, 'attributes'));
+ if (this.id) attrs.id = _.result(this, 'id');
+ if (this.className) attrs['class'] = _.result(this, 'className');
+ this.setElement(this._createElement(_.result(this, 'tagName')));
+ this._setAttributes(attrs);
+ } else {
+ this.setElement(_.result(this, 'el'));
+ }
+ },
+
+ // Set attributes from a hash on this view's element. Exposed for
+ // subclasses using an alternative DOM manipulation API.
+ _setAttributes: function(attributes) {
+ this.$el.attr(attributes);
+ }
+
+ });
+
+ // Backbone.sync
+ // -------------
+
+ // Override this function to change the manner in which Backbone persists
+ // models to the server. You will be passed the type of request, and the
+ // model in question. By default, makes a RESTful Ajax request
+ // to the model's `url()`. Some possible customizations could be:
+ //
+ // * Use `setTimeout` to batch rapid-fire updates into a single request.
+ // * Send up the models as XML instead of JSON.
+ // * Persist models via WebSockets instead of Ajax.
+ //
+ // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
+ // as `POST`, with a `_method` parameter containing the true HTTP method,
+ // as well as all requests with the body as `application/x-www-form-urlencoded`
+ // instead of `application/json` with the model in a param named `model`.
+ // Useful when interfacing with server-side languages like **PHP** that make
+ // it difficult to read the body of `PUT` requests.
+ Backbone.sync = function(method, model, options) {
+ var type = methodMap[method];
+
+ // Default options, unless specified.
+ _.defaults(options || (options = {}), {
+ emulateHTTP: Backbone.emulateHTTP,
+ emulateJSON: Backbone.emulateJSON
+ });
+
+ // Default JSON-request options.
+ var params = {type: type, dataType: 'json'};
+
+ // Ensure that we have a URL.
+ if (!options.url) {
+ params.url = _.result(model, 'url') || urlError();
+ }
+
+ // Ensure that we have the appropriate request data.
+ if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
+ params.contentType = 'application/json';
+ params.data = JSON.stringify(options.attrs || model.toJSON(options));
+ }
+
+ // For older servers, emulate JSON by encoding the request into an HTML-form.
+ if (options.emulateJSON) {
+ params.contentType = 'application/x-www-form-urlencoded';
+ params.data = params.data ? {model: params.data} : {};
+ }
+
+ // For older servers, emulate HTTP by mimicking the HTTP method with `_method`
+ // And an `X-HTTP-Method-Override` header.
+ if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
+ params.type = 'POST';
+ if (options.emulateJSON) params.data._method = type;
+ var beforeSend = options.beforeSend;
+ options.beforeSend = function(xhr) {
+ xhr.setRequestHeader('X-HTTP-Method-Override', type);
+ if (beforeSend) return beforeSend.apply(this, arguments);
+ };
+ }
+
+ // Don't process data on a non-GET request.
+ if (params.type !== 'GET' && !options.emulateJSON) {
+ params.processData = false;
+ }
+
+ // Pass along `textStatus` and `errorThrown` from jQuery.
+ var error = options.error;
+ options.error = function(xhr, textStatus, errorThrown) {
+ options.textStatus = textStatus;
+ options.errorThrown = errorThrown;
+ if (error) error.call(options.context, xhr, textStatus, errorThrown);
+ };
+
+ // Make the request, allowing the user to override any Ajax options.
+ var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
+ model.trigger('request', model, xhr, options);
+ return xhr;
+ };
+
+ // Map from CRUD to HTTP for our default `Backbone.sync` implementation.
+ var methodMap = {
+ 'create': 'POST',
+ 'update': 'PUT',
+ 'patch': 'PATCH',
+ 'delete': 'DELETE',
+ 'read': 'GET'
+ };
+
+ // Set the default implementation of `Backbone.ajax` to proxy through to `$`.
+ // Override this if you'd like to use a different library.
+ Backbone.ajax = function() {
+ return Backbone.$.ajax.apply(Backbone.$, arguments);
+ };
+
+ // Backbone.Router
+ // ---------------
+
+ // Routers map faux-URLs to actions, and fire events when routes are
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
+ var Router = Backbone.Router = function(options) {
+ options || (options = {});
+ if (options.routes) this.routes = options.routes;
+ this._bindRoutes();
+ this.initialize.apply(this, arguments);
+ };
+
+ // Cached regular expressions for matching named param parts and splatted
+ // parts of route strings.
+ var optionalParam = /\((.*?)\)/g;
+ var namedParam = /(\(\?)?:\w+/g;
+ var splatParam = /\*\w+/g;
+ var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
+
+ // Set up all inheritable **Backbone.Router** properties and methods.
+ _.extend(Router.prototype, Events, {
+
+ // Initialize is an empty function by default. Override it with your own
+ // initialization logic.
+ initialize: function(){},
+
+ // Manually bind a single named route to a callback. For example:
+ //
+ // this.route('search/:query/p:num', 'search', function(query, num) {
+ // ...
+ // });
+ //
+ route: function(route, name, callback) {
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+ if (_.isFunction(name)) {
+ callback = name;
+ name = '';
+ }
+ if (!callback) callback = this[name];
+ var router = this;
+ Backbone.history.route(route, function(fragment) {
+ var args = router._extractParameters(route, fragment);
+ if (router.execute(callback, args, name) !== false) {
+ router.trigger.apply(router, ['route:' + name].concat(args));
+ router.trigger('route', name, args);
+ Backbone.history.trigger('route', router, name, args);
+ }
+ });
+ return this;
+ },
+
+ // Execute a route handler with the provided parameters. This is an
+ // excellent place to do pre-route setup or post-route cleanup.
+ execute: function(callback, args, name) {
+ if (callback) callback.apply(this, args);
+ },
+
+ // Simple proxy to `Backbone.history` to save a fragment into the history.
+ navigate: function(fragment, options) {
+ Backbone.history.navigate(fragment, options);
+ return this;
+ },
+
+ // Bind all defined routes to `Backbone.history`. We have to reverse the
+ // order of the routes here to support behavior where the most general
+ // routes can be defined at the bottom of the route map.
+ _bindRoutes: function() {
+ if (!this.routes) return;
+ this.routes = _.result(this, 'routes');
+ var route, routes = _.keys(this.routes);
+ while ((route = routes.pop()) != null) {
+ this.route(route, this.routes[route]);
+ }
+ },
+
+ // Convert a route string into a regular expression, suitable for matching
+ // against the current location hash.
+ _routeToRegExp: function(route) {
+ route = route.replace(escapeRegExp, '\\$&')
+ .replace(optionalParam, '(?:$1)?')
+ .replace(namedParam, function(match, optional) {
+ return optional ? match : '([^/?]+)';
+ })
+ .replace(splatParam, '([^?]*?)');
+ return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
+ },
+
+ // Given a route, and a URL fragment that it matches, return the array of
+ // extracted decoded parameters. Empty or unmatched parameters will be
+ // treated as `null` to normalize cross-browser behavior.
+ _extractParameters: function(route, fragment) {
+ var params = route.exec(fragment).slice(1);
+ return _.map(params, function(param, i) {
+ // Don't decode the search params.
+ if (i === params.length - 1) return param || null;
+ return param ? decodeURIComponent(param) : null;
+ });
+ }
+
+ });
+
+ // Backbone.History
+ // ----------------
+
+ // Handles cross-browser history management, based on either
+ // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
+ // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
+ // and URL fragments. If the browser supports neither (old IE, natch),
+ // falls back to polling.
+ var History = Backbone.History = function() {
+ this.handlers = [];
+ _.bindAll(this, 'checkUrl');
+
+ // Ensure that `History` can be used outside of the browser.
+ if (typeof window !== 'undefined') {
+ this.location = window.location;
+ this.history = window.history;
+ }
+ };
+
+ // Cached regex for stripping a leading hash/slash and trailing space.
+ var routeStripper = /^[#\/]|\s+$/g;
+
+ // Cached regex for stripping leading and trailing slashes.
+ var rootStripper = /^\/+|\/+$/g;
+
+ // Cached regex for stripping urls of hash.
+ var pathStripper = /#.*$/;
+
+ // Has the history handling already been started?
+ History.started = false;
+
+ // Set up all inheritable **Backbone.History** properties and methods.
+ _.extend(History.prototype, Events, {
+
+ // The default interval to poll for hash changes, if necessary, is
+ // twenty times a second.
+ interval: 50,
+
+ // Are we at the app root?
+ atRoot: function() {
+ var path = this.location.pathname.replace(/[^\/]$/, '$&/');
+ return path === this.root && !this.getSearch();
+ },
+
+ // Does the pathname match the root?
+ matchRoot: function() {
+ var path = this.decodeFragment(this.location.pathname);
+ var root = path.slice(0, this.root.length - 1) + '/';
+ return root === this.root;
+ },
+
+ // Unicode characters in `location.pathname` are percent encoded so they're
+ // decoded for comparison. `%25` should not be decoded since it may be part
+ // of an encoded parameter.
+ decodeFragment: function(fragment) {
+ return decodeURI(fragment.replace(/%25/g, '%2525'));
+ },
+
+ // In IE6, the hash fragment and search params are incorrect if the
+ // fragment contains `?`.
+ getSearch: function() {
+ var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
+ return match ? match[0] : '';
+ },
+
+ // Gets the true hash value. Cannot use location.hash directly due to bug
+ // in Firefox where location.hash will always be decoded.
+ getHash: function(window) {
+ var match = (window || this).location.href.match(/#(.*)$/);
+ return match ? match[1] : '';
+ },
+
+ // Get the pathname and search params, without the root.
+ getPath: function() {
+ var path = this.decodeFragment(
+ this.location.pathname + this.getSearch()
+ ).slice(this.root.length - 1);
+ return path.charAt(0) === '/' ? path.slice(1) : path;
+ },
+
+ // Get the cross-browser normalized URL fragment from the path or hash.
+ getFragment: function(fragment) {
+ if (fragment == null) {
+ if (this._usePushState || !this._wantsHashChange) {
+ fragment = this.getPath();
+ } else {
+ fragment = this.getHash();
+ }
+ }
+ return fragment.replace(routeStripper, '');
+ },
+
+ // Start the hash change handling, returning `true` if the current URL matches
+ // an existing route, and `false` otherwise.
+ start: function(options) {
+ if (History.started) throw new Error('Backbone.history has already been started');
+ History.started = true;
+
+ // Figure out the initial configuration. Do we need an iframe?
+ // Is pushState desired ... is it available?
+ this.options = _.extend({root: '/'}, this.options, options);
+ this.root = this.options.root;
+ this._wantsHashChange = this.options.hashChange !== false;
+ this._hasHashChange = 'onhashchange' in window;
+ this._useHashChange = this._wantsHashChange && this._hasHashChange;
+ this._wantsPushState = !!this.options.pushState;
+ this._hasPushState = !!(this.history && this.history.pushState);
+ this._usePushState = this._wantsPushState && this._hasPushState;
+ this.fragment = this.getFragment();
+
+ // Normalize root to always include a leading and trailing slash.
+ this.root = ('/' + this.root + '/').replace(rootStripper, '/');
+
+ // Transition from hashChange to pushState or vice versa if both are
+ // requested.
+ if (this._wantsHashChange && this._wantsPushState) {
+
+ // If we've started off with a route from a `pushState`-enabled
+ // browser, but we're currently in a browser that doesn't support it...
+ if (!this._hasPushState && !this.atRoot()) {
+ var root = this.root.slice(0, -1) || '/';
+ this.location.replace(root + '#' + this.getPath());
+ // Return immediately as browser will do redirect to new url
+ return true;
+
+ // Or if we've started out with a hash-based route, but we're currently
+ // in a browser where it could be `pushState`-based instead...
+ } else if (this._hasPushState && this.atRoot()) {
+ this.navigate(this.getHash(), {replace: true});
+ }
+
+ }
+
+ // Proxy an iframe to handle location events if the browser doesn't
+ // support the `hashchange` event, HTML5 history, or the user wants
+ // `hashChange` but not `pushState`.
+ if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
+ this.iframe = document.createElement('iframe');
+ this.iframe.src = 'javascript:0';
+ this.iframe.style.display = 'none';
+ this.iframe.tabIndex = -1;
+ var body = document.body;
+ // Using `appendChild` will throw on IE < 9 if the document is not ready.
+ var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
+ iWindow.document.open();
+ iWindow.document.close();
+ iWindow.location.hash = '#' + this.fragment;
+ }
+
+ // Add a cross-platform `addEventListener` shim for older browsers.
+ var addEventListener = window.addEventListener || function (eventName, listener) {
+ return attachEvent('on' + eventName, listener);
+ };
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
+ if (this._usePushState) {
+ addEventListener('popstate', this.checkUrl, false);
+ } else if (this._useHashChange && !this.iframe) {
+ addEventListener('hashchange', this.checkUrl, false);
+ } else if (this._wantsHashChange) {
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+ }
+
+ if (!this.options.silent) return this.loadUrl();
+ },
+
+ // Disable Backbone.history, perhaps temporarily. Not useful in a real app,
+ // but possibly useful for unit testing Routers.
+ stop: function() {
+ // Add a cross-platform `removeEventListener` shim for older browsers.
+ var removeEventListener = window.removeEventListener || function (eventName, listener) {
+ return detachEvent('on' + eventName, listener);
+ };
+
+ // Remove window listeners.
+ if (this._usePushState) {
+ removeEventListener('popstate', this.checkUrl, false);
+ } else if (this._useHashChange && !this.iframe) {
+ removeEventListener('hashchange', this.checkUrl, false);
+ }
+
+ // Clean up the iframe if necessary.
+ if (this.iframe) {
+ document.body.removeChild(this.iframe);
+ this.iframe = null;
+ }
+
+ // Some environments will throw when clearing an undefined interval.
+ if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
+ History.started = false;
+ },
+
+ // Add a route to be tested when the fragment changes. Routes added later
+ // may override previous routes.
+ route: function(route, callback) {
+ this.handlers.unshift({route: route, callback: callback});
+ },
+
+ // Checks the current URL to see if it has changed, and if it has,
+ // calls `loadUrl`, normalizing across the hidden iframe.
+ checkUrl: function(e) {
+ var current = this.getFragment();
+
+ // If the user pressed the back button, the iframe's hash will have
+ // changed and we should use that for comparison.
+ if (current === this.fragment && this.iframe) {
+ current = this.getHash(this.iframe.contentWindow);
+ }
+
+ if (current === this.fragment) return false;
+ if (this.iframe) this.navigate(current);
+ this.loadUrl();
+ },
+
+ // Attempt to load the current URL fragment. If a route succeeds with a
+ // match, returns `true`. If no defined routes matches the fragment,
+ // returns `false`.
+ loadUrl: function(fragment) {
+ // If the root doesn't match, no routes can match either.
+ if (!this.matchRoot()) return false;
+ fragment = this.fragment = this.getFragment(fragment);
+ return _.any(this.handlers, function(handler) {
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
+ return true;
+ }
+ });
+ },
+
+ // Save a fragment into the hash history, or replace the URL state if the
+ // 'replace' option is passed. You are responsible for properly URL-encoding
+ // the fragment in advance.
+ //
+ // The options object can contain `trigger: true` if you wish to have the
+ // route callback be fired (not usually desirable), or `replace: true`, if
+ // you wish to modify the current URL without adding an entry to the history.
+ navigate: function(fragment, options) {
+ if (!History.started) return false;
+ if (!options || options === true) options = {trigger: !!options};
+
+ // Normalize the fragment.
+ fragment = this.getFragment(fragment || '');
+
+ // Don't include a trailing slash on the root.
+ var root = this.root;
+ if (fragment === '' || fragment.charAt(0) === '?') {
+ root = root.slice(0, -1) || '/';
+ }
+ var url = root + fragment;
+
+ // Strip the hash and decode for matching.
+ fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
+
+ if (this.fragment === fragment) return;
+ this.fragment = fragment;
+
+ // If pushState is available, we use it to set the fragment as a real URL.
+ if (this._usePushState) {
+ this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+ // If hash changes haven't been explicitly disabled, update the hash
+ // fragment to store history.
+ } else if (this._wantsHashChange) {
+ this._updateHash(this.location, fragment, options.replace);
+ if (this.iframe && (fragment !== this.getHash(this.iframe.contentWindow))) {
+ var iWindow = this.iframe.contentWindow;
+
+ // Opening and closing the iframe tricks IE7 and earlier to push a
+ // history entry on hash-tag change. When replace is true, we don't
+ // want this.
+ if (!options.replace) {
+ iWindow.document.open();
+ iWindow.document.close();
+ }
+
+ this._updateHash(iWindow.location, fragment, options.replace);
+ }
+
+ // If you've told us that you explicitly don't want fallback hashchange-
+ // based history, then `navigate` becomes a page refresh.
+ } else {
+ return this.location.assign(url);
+ }
+ if (options.trigger) return this.loadUrl(fragment);
+ },
+
+ // Update the hash location, either replacing the current entry, or adding
+ // a new one to the browser history.
+ _updateHash: function(location, fragment, replace) {
+ if (replace) {
+ var href = location.href.replace(/(javascript:|#).*$/, '');
+ location.replace(href + '#' + fragment);
+ } else {
+ // Some browsers require that `hash` contains a leading #.
+ location.hash = '#' + fragment;
+ }
+ }
+
+ });
+
+ // Create the default Backbone.history.
+ Backbone.history = new History;
+
+ // Helpers
+ // -------
+
+ // Helper function to correctly set up the prototype chain for subclasses.
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
+ // class properties to be extended.
+ var extend = function(protoProps, staticProps) {
+ var parent = this;
+ var child;
+
+ // The constructor function for the new subclass is either defined by you
+ // (the "constructor" property in your `extend` definition), or defaulted
+ // by us to simply call the parent constructor.
+ if (protoProps && _.has(protoProps, 'constructor')) {
+ child = protoProps.constructor;
+ } else {
+ child = function(){ return parent.apply(this, arguments); };
+ }
+
+ // Add static properties to the constructor function, if supplied.
+ _.extend(child, parent, staticProps);
+
+ // Set the prototype chain to inherit from `parent`, without calling
+ // `parent` constructor function.
+ var Surrogate = function(){ this.constructor = child; };
+ Surrogate.prototype = parent.prototype;
+ child.prototype = new Surrogate;
+
+ // Add prototype properties (instance properties) to the subclass,
+ // if supplied.
+ if (protoProps) _.extend(child.prototype, protoProps);
+
+ // Set a convenience property in case the parent's prototype is needed
+ // later.
+ child.__super__ = parent.prototype;
+
+ return child;
+ };
+
+ // Set up inheritance for the model, collection, router, view and history.
+ Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
+
+ // Throw an error when a URL is needed, and none is supplied.
+ var urlError = function() {
+ throw new Error('A "url" property or function must be specified');
+ };
+
+ // Wrap an optional error callback with a fallback error event.
+ var wrapError = function(model, options) {
+ var error = options.error;
+ options.error = function(resp) {
+ if (error) error.call(options.context, model, resp, options);
+ model.trigger('error', model, resp, options);
+ };
+ };
+
+ return Backbone;
+
+}));
diff --git a/db_structure.xml b/db_structure.xml
index 6d1cf6973c5..870c0ab018d 100644
--- a/db_structure.xml
+++ b/db_structure.xml
@@ -102,6 +102,18 @@
<length>4</length>
</field>
+ <field>
+ <name>available</name>
+ <type>boolean</type>
+ <default>true</default>
+ <notnull>true</notnull>
+ </field>
+
+ <field>
+ <name>last_checked</name>
+ <type>integer</type>
+ </field>
+
<index>
<name>storages_id_index</name>
<unique>true</unique>
diff --git a/lib/base.php b/lib/base.php
index 299970e269c..c0f3e50142e 100644
--- a/lib/base.php
+++ b/lib/base.php
@@ -407,6 +407,8 @@ class OC {
OC_Util::addScript('mimetype');
OC_Util::addScript('mimetypelist');
OC_Util::addVendorScript('snapjs/dist/latest/snap');
+ OC_Util::addVendorScript('core', 'backbone/backbone');
+ OC_Util::addScript('oc-backbone');
// avatars
if (\OC::$server->getSystemConfig()->getValue('enable_avatars', true) === true) {
diff --git a/lib/private/api.php b/lib/private/api.php
index 8e483b7efe9..fb2110471b2 100644
--- a/lib/private/api.php
+++ b/lib/private/api.php
@@ -1,4 +1,7 @@
<?php
+use OCP\API;
+use OCP\AppFramework\Http;
+
/**
* @author Bart Visscher <bartv@thisnet.nl>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
@@ -82,7 +85,7 @@ class OC_API {
* @param array $requirements
*/
public static function register($method, $url, $action, $app,
- $authLevel = \OCP\API::USER_AUTH,
+ $authLevel = API::USER_AUTH,
$defaults = array(),
$requirements = array()) {
$name = strtolower($method).$url;
@@ -123,7 +126,7 @@ class OC_API {
if(!self::isAuthorised($action)) {
$responses[] = array(
'app' => $action['app'],
- 'response' => new OC_OCS_Result(null, \OCP\API::RESPOND_UNAUTHORISED, 'Unauthorised'),
+ 'response' => new OC_OCS_Result(null, API::RESPOND_UNAUTHORISED, 'Unauthorised'),
'shipped' => OC_App::isShipped($action['app']),
);
continue;
@@ -131,7 +134,7 @@ class OC_API {
if(!is_callable($action['action'])) {
$responses[] = array(
'app' => $action['app'],
- 'response' => new OC_OCS_Result(null, \OCP\API::RESPOND_NOT_FOUND, 'Api method not found'),
+ 'response' => new OC_OCS_Result(null, API::RESPOND_NOT_FOUND, 'Api method not found'),
'shipped' => OC_App::isShipped($action['app']),
);
continue;
@@ -252,15 +255,15 @@ class OC_API {
private static function isAuthorised($action) {
$level = $action['authlevel'];
switch($level) {
- case \OCP\API::GUEST_AUTH:
+ case API::GUEST_AUTH:
// Anyone can access
return true;
break;
- case \OCP\API::USER_AUTH:
+ case API::USER_AUTH:
// User required
return self::loginUser();
break;
- case \OCP\API::SUBADMIN_AUTH:
+ case API::SUBADMIN_AUTH:
// Check for subadmin
$user = self::loginUser();
if(!$user) {
@@ -275,7 +278,7 @@ class OC_API {
}
}
break;
- case \OCP\API::ADMIN_AUTH:
+ case API::ADMIN_AUTH:
// Check for admin
$user = self::loginUser();
if(!$user) {
@@ -342,28 +345,25 @@ class OC_API {
*/
public static function respond($result, $format='xml') {
// Send 401 headers if unauthorised
- if($result->getStatusCode() === \OCP\API::RESPOND_UNAUTHORISED) {
+ if($result->getStatusCode() === API::RESPOND_UNAUTHORISED) {
header('WWW-Authenticate: Basic realm="Authorisation Required"');
header('HTTP/1.0 401 Unauthorized');
}
- $response = array(
- 'ocs' => array(
- 'meta' => $result->getMeta(),
- 'data' => $result->getData(),
- ),
- );
- if ($format == 'json') {
- OC_JSON::encodedPrint($response);
- } else if ($format == 'xml') {
- header('Content-type: text/xml; charset=UTF-8');
- $writer = new XMLWriter();
- $writer->openMemory();
- $writer->setIndent( true );
- $writer->startDocument();
- self::toXML($response, $writer);
- $writer->endDocument();
- echo $writer->outputMemory(true);
+
+ foreach($result->getHeaders() as $name => $value) {
+ header($name . ': ' . $value);
}
+
+ if (self::isV2()) {
+ $statusCode = self::mapStatusCodes($result->getStatusCode());
+ if (!is_null($statusCode)) {
+ OC_Response::setStatus($statusCode);
+ }
+ }
+
+ self::setContentType($format);
+ $body = self::renderResult($result, $format);
+ echo $body;
}
/**
@@ -400,8 +400,8 @@ class OC_API {
/**
* Based on the requested format the response content type is set
*/
- public static function setContentType() {
- $format = self::requestedFormat();
+ public static function setContentType($format = null) {
+ $format = is_null($format) ? self::requestedFormat() : $format;
if ($format === 'xml') {
header('Content-type: text/xml; charset=UTF-8');
return;
@@ -415,5 +415,64 @@ class OC_API {
header('Content-Type: application/octet-stream; charset=utf-8');
}
+ /**
+ * @return boolean
+ */
+ private static function isV2() {
+ $request = \OC::$server->getRequest();
+ $script = $request->getScriptName();
+
+ return $script === '/ocs/v2.php';
+ }
+
+ /**
+ * @param integer $sc
+ * @return int
+ */
+ public static function mapStatusCodes($sc) {
+ switch ($sc) {
+ case API::RESPOND_NOT_FOUND:
+ return Http::STATUS_NOT_FOUND;
+ case API::RESPOND_SERVER_ERROR:
+ return Http::STATUS_INTERNAL_SERVER_ERROR;
+ case API::RESPOND_UNKNOWN_ERROR:
+ return Http::STATUS_INTERNAL_SERVER_ERROR;
+ case API::RESPOND_UNAUTHORISED:
+ // already handled for v1
+ return null;
+ case 100:
+ return Http::STATUS_OK;
+ }
+ // any 2xx, 4xx and 5xx will be used as is
+ if ($sc >= 200 && $sc < 600) {
+ return $sc;
+ }
+
+ return Http::STATUS_BAD_REQUEST;
+ }
+ /**
+ * @param OC_OCS_Result $result
+ * @param string $format
+ * @return string
+ */
+ public static function renderResult($result, $format) {
+ $response = array(
+ 'ocs' => array(
+ 'meta' => $result->getMeta(),
+ 'data' => $result->getData(),
+ ),
+ );
+ if ($format == 'json') {
+ return OC_JSON::encode($response);
+ }
+
+ $writer = new XMLWriter();
+ $writer->openMemory();
+ $writer->setIndent(true);
+ $writer->startDocument();
+ self::toXML($response, $writer);
+ $writer->endDocument();
+ return $writer->outputMemory(true);
+ }
}
diff --git a/lib/private/app.php b/lib/private/app.php
index 6c6f79dfa9d..e51fe73cb19 100644
--- a/lib/private/app.php
+++ b/lib/private/app.php
@@ -74,6 +74,16 @@ class OC_App {
}
/**
+ * Check if an app is loaded
+ *
+ * @param string $app
+ * @return bool
+ */
+ public static function isAppLoaded($app) {
+ return in_array($app, self::$loadedApps, true);
+ }
+
+ /**
* loads all apps
*
* @param array $types
@@ -421,6 +431,7 @@ class OC_App {
*/
public static function getSettingsNavigation() {
$l = \OC::$server->getL10N('lib');
+ $defaults = new OC_Defaults();
$settings = array();
// by default, settings only contain the help menu
@@ -431,7 +442,7 @@ class OC_App {
array(
"id" => "help",
"order" => 1000,
- "href" => OC_Helper::linkToRoute("settings_help"),
+ "href" => $defaults->getKnowledgeBaseUrl(),
"name" => $l->t("Help"),
"icon" => OC_Helper::imagePath("settings", "help.svg")
)
diff --git a/lib/private/appframework/dependencyinjection/dicontainer.php b/lib/private/appframework/dependencyinjection/dicontainer.php
index c7ce6545972..c66b792064d 100644
--- a/lib/private/appframework/dependencyinjection/dicontainer.php
+++ b/lib/private/appframework/dependencyinjection/dicontainer.php
@@ -96,6 +96,10 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return $this->getServer()->getMemCacheFactory();
});
+ $this->registerService('OC\\CapabilitiesManager', function($c) {
+ return $this->getServer()->getCapabilitiesManager();
+ });
+
$this->registerService('OCP\\IConfig', function($c) {
return $this->getServer()->getConfig();
});
@@ -212,6 +216,10 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return $this->getServer()->getUserSession();
});
+ $this->registerService('OCP\\ISession', function($c) {
+ return $this->getServer()->getSession();
+ });
+
$this->registerService('ServerContainer', function ($c) {
return $this->getServer();
});
@@ -386,5 +394,15 @@ class DIContainer extends SimpleContainer implements IAppContainer {
\OCP\Util::writeLog($this->getAppName(), $message, $level);
}
+ /**
+ * Register a capability
+ *
+ * @param string $serviceName e.g. 'OCA\Files\Capabilities'
+ */
+ public function registerCapability($serviceName) {
+ $this->query('OC\CapabilitiesManager')->registerCapability(function() use ($serviceName) {
+ return $this->query($serviceName);
+ });
+ }
}
diff --git a/lib/private/capabilitiesmanager.php b/lib/private/capabilitiesmanager.php
new file mode 100644
index 00000000000..74154f2c631
--- /dev/null
+++ b/lib/private/capabilitiesmanager.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OC;
+
+
+use OCP\Capabilities\ICapability;
+
+class CapabilitiesManager {
+
+ /**
+ * @var \Closure[]
+ */
+ private $capabilities = array();
+
+ /**
+ * Get an array of al the capabilities that are registered at this manager
+ *
+ * @throws \InvalidArgumentException
+ * @return array
+ */
+ public function getCapabilities() {
+ $capabilities = [];
+ foreach($this->capabilities as $capability) {
+ $c = $capability();
+ if ($c instanceof ICapability) {
+ $capabilities = array_replace_recursive($capabilities, $c->getCapabilities());
+ } else {
+ throw new \InvalidArgumentException('The given Capability (' . get_class($c) . ') does not implement the ICapability interface');
+ }
+ }
+
+ return $capabilities;
+ }
+
+ /**
+ * In order to improve lazy loading a closure can be registered which will be called in case
+ * capabilities are actually requested
+ *
+ * $callable has to return an instance of OCP\Capabilities\ICapability
+ *
+ * @param \Closure $callable
+ */
+ public function registerCapability(\Closure $callable) {
+ array_push($this->capabilities, $callable);
+ }
+}
diff --git a/lib/private/connector/sabre/file.php b/lib/private/connector/sabre/file.php
index 18bd3b8d91d..fa2f5ce18d7 100644
--- a/lib/private/connector/sabre/file.php
+++ b/lib/private/connector/sabre/file.php
@@ -208,10 +208,9 @@ class File extends Node implements IFile {
}
// since we skipped the view we need to scan and emit the hooks ourselves
- $partStorage->getScanner()->scanFile($internalPath);
+ $this->fileView->getUpdater()->update($this->path);
if ($view) {
- $this->fileView->getUpdater()->propagate($hookPath);
if (!$exists) {
\OC_Hook::emit(\OC\Files\Filesystem::CLASSNAME, \OC\Files\Filesystem::signal_post_create, array(
\OC\Files\Filesystem::signal_param_path => $hookPath
diff --git a/lib/private/db/querybuilder/querybuilder.php b/lib/private/db/querybuilder/querybuilder.php
index 1a1408876f1..1d97faf77cc 100644
--- a/lib/private/db/querybuilder/querybuilder.php
+++ b/lib/private/db/querybuilder/querybuilder.php
@@ -37,6 +37,9 @@ class QueryBuilder implements IQueryBuilder {
/** @var QuoteHelper */
private $helper;
+ /** @var bool */
+ private $automaticTablePrefix = true;
+
/**
* Initializes a new QueryBuilder.
*
@@ -49,6 +52,17 @@ class QueryBuilder implements IQueryBuilder {
}
/**
+ * Enable/disable automatic prefixing of table names with the oc_ prefix
+ *
+ * @param bool $enabled If set to true table names will be prefixed with the
+ * owncloud database prefix automatically.
+ * @since 8.2.0
+ */
+ public function automaticTablePrefix($enabled) {
+ $this->automaticTablePrefix = (bool) $enabled;
+ }
+
+ /**
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
* This producer method is intended for convenient inline usage. Example:
*
@@ -329,7 +343,7 @@ class QueryBuilder implements IQueryBuilder {
*/
public function delete($delete = null, $alias = null) {
$this->queryBuilder->delete(
- $this->helper->quoteColumnName($delete),
+ $this->getTableName($delete),
$alias
);
@@ -354,7 +368,7 @@ class QueryBuilder implements IQueryBuilder {
*/
public function update($update = null, $alias = null) {
$this->queryBuilder->update(
- $this->helper->quoteColumnName($update),
+ $this->getTableName($update),
$alias
);
@@ -382,7 +396,7 @@ class QueryBuilder implements IQueryBuilder {
*/
public function insert($insert = null) {
$this->queryBuilder->insert(
- $this->helper->quoteColumnName($insert)
+ $this->getTableName($insert)
);
return $this;
@@ -405,7 +419,7 @@ class QueryBuilder implements IQueryBuilder {
*/
public function from($from, $alias = null) {
$this->queryBuilder->from(
- $this->helper->quoteColumnName($from),
+ $this->getTableName($from),
$alias
);
@@ -432,7 +446,7 @@ class QueryBuilder implements IQueryBuilder {
public function join($fromAlias, $join, $alias, $condition = null) {
$this->queryBuilder->join(
$fromAlias,
- $this->helper->quoteColumnName($join),
+ $this->getTableName($join),
$alias,
$condition
);
@@ -460,7 +474,7 @@ class QueryBuilder implements IQueryBuilder {
public function innerJoin($fromAlias, $join, $alias, $condition = null) {
$this->queryBuilder->innerJoin(
$fromAlias,
- $this->helper->quoteColumnName($join),
+ $this->getTableName($join),
$alias,
$condition
);
@@ -488,7 +502,7 @@ class QueryBuilder implements IQueryBuilder {
public function leftJoin($fromAlias, $join, $alias, $condition = null) {
$this->queryBuilder->leftJoin(
$fromAlias,
- $this->helper->quoteColumnName($join),
+ $this->getTableName($join),
$alias,
$condition
);
@@ -516,7 +530,7 @@ class QueryBuilder implements IQueryBuilder {
public function rightJoin($fromAlias, $join, $alias, $condition = null) {
$this->queryBuilder->rightJoin(
$fromAlias,
- $this->helper->quoteColumnName($join),
+ $this->getTableName($join),
$alias,
$condition
);
@@ -984,4 +998,16 @@ class QueryBuilder implements IQueryBuilder {
public function createFunction($call) {
return new QueryFunction($call);
}
+
+ /**
+ * @param string $table
+ * @return string
+ */
+ private function getTableName($table) {
+ if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) {
+ return $this->helper->quoteColumnName($table);
+ }
+
+ return $this->helper->quoteColumnName('*PREFIX*' . $table);
+ }
}
diff --git a/lib/private/defaults.php b/lib/private/defaults.php
index 16f45943f54..b86805357bd 100644
--- a/lib/private/defaults.php
+++ b/lib/private/defaults.php
@@ -46,9 +46,11 @@ class OC_Defaults {
private $defaultSlogan;
private $defaultLogoClaim;
private $defaultMailHeaderColor;
+ private $defaultKnowledgeBaseUrl;
function __construct() {
$this->l = \OC::$server->getL10N('lib');
+ $urlGenerator = \OC::$server->getURLGenerator();
$version = OC_Util::getVersion();
$this->defaultEntity = 'ownCloud'; /* e.g. company name, used for footers and copyright notices */
@@ -64,6 +66,7 @@ class OC_Defaults {
$this->defaultSlogan = $this->l->t('web services under your control');
$this->defaultLogoClaim = '';
$this->defaultMailHeaderColor = '#1d2d44'; /* header color of mail notifications */
+ $this->defaultKnowledgeBaseUrl = $urlGenerator->linkToRoute('settings_help');
$themePath = OC::$SERVERROOT . '/themes/' . OC_Util::getTheme() . '/defaults.php';
if (file_exists($themePath)) {
@@ -79,6 +82,7 @@ class OC_Defaults {
/**
* @param string $method
+ * @return bool
*/
private function themeExist($method) {
if (isset($this->theme) && method_exists($this->theme, $method)) {
@@ -280,4 +284,19 @@ class OC_Defaults {
}
}
+ /**
+ * get knowledge base URL, will be used for the "Help"-Link in the top
+ * right menu
+ *
+ * @return string
+ */
+ public function getKnowledgeBaseUrl() {
+ if ($this->themeExist('getKnowledgeBaseUrl')) {
+ return $this->theme->getKnowledgeBaseUrl();
+ } else {
+ return $this->defaultKnowledgeBaseUrl;
+ }
+
+ }
+
}
diff --git a/lib/private/files/cache/storage.php b/lib/private/files/cache/storage.php
index ebef245f399..338d8308281 100644
--- a/lib/private/files/cache/storage.php
+++ b/lib/private/files/cache/storage.php
@@ -43,9 +43,10 @@ class Storage {
/**
* @param \OC\Files\Storage\Storage|string $storage
+ * @param bool $isAvailable
* @throws \RuntimeException
*/
- public function __construct($storage) {
+ public function __construct($storage, $isAvailable = true) {
if ($storage instanceof \OC\Files\Storage\Storage) {
$this->storageId = $storage->getId();
} else {
@@ -53,17 +54,14 @@ class Storage {
}
$this->storageId = self::adjustStorageId($this->storageId);
- $sql = 'SELECT `numeric_id` FROM `*PREFIX*storages` WHERE `id` = ?';
- $result = \OC_DB::executeAudited($sql, array($this->storageId));
- if ($row = $result->fetchRow()) {
+ if ($row = self::getStorageById($this->storageId)) {
$this->numericId = $row['numeric_id'];
} else {
$connection = \OC_DB::getConnection();
- if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId])) {
+ if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $isAvailable])) {
$this->numericId = \OC_DB::insertid('*PREFIX*storages');
} else {
- $result = \OC_DB::executeAudited($sql, array($this->storageId));
- if ($row = $result->fetchRow()) {
+ if ($row = self::getStorageById($this->storageId)) {
$this->numericId = $row['numeric_id'];
} else {
throw new \RuntimeException('Storage could neither be inserted nor be selected from the database');
@@ -73,6 +71,16 @@ class Storage {
}
/**
+ * @param string $storageId
+ * @return array|null
+ */
+ public static function getStorageById($storageId) {
+ $sql = 'SELECT * FROM `*PREFIX*storages` WHERE `id` = ?';
+ $result = \OC_DB::executeAudited($sql, array($storageId));
+ return $result->fetchRow();
+ }
+
+ /**
* Adjusts the storage id to use md5 if too long
* @param string $storageId storage id
* @return string unchanged $storageId if its length is less than 64 characters,
@@ -120,9 +128,7 @@ class Storage {
public static function getNumericStorageId($storageId) {
$storageId = self::adjustStorageId($storageId);
- $sql = 'SELECT `numeric_id` FROM `*PREFIX*storages` WHERE `id` = ?';
- $result = \OC_DB::executeAudited($sql, array($storageId));
- if ($row = $result->fetchRow()) {
+ if ($row = self::getStorageById($storageId)) {
return $row['numeric_id'];
} else {
return null;
@@ -130,6 +136,28 @@ class Storage {
}
/**
+ * @return array|null [ available, last_checked ]
+ */
+ public function getAvailability() {
+ if ($row = self::getStorageById($this->storageId)) {
+ return [
+ 'available' => $row['available'],
+ 'last_checked' => $row['last_checked']
+ ];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @param bool $isAvailable
+ */
+ public function setAvailability($isAvailable) {
+ $sql = 'UPDATE `*PREFIX*storages` SET `available` = ?, `last_checked` = ? WHERE `id` = ?';
+ \OC_DB::executeAudited($sql, array($isAvailable, time(), $this->storageId));
+ }
+
+ /**
* Check if a string storage id is known
*
* @param string $storageId
diff --git a/lib/private/files/mount/mountpoint.php b/lib/private/files/mount/mountpoint.php
index 2871bbd9083..5e4949aa9dd 100644
--- a/lib/private/files/mount/mountpoint.php
+++ b/lib/private/files/mount/mountpoint.php
@@ -29,6 +29,7 @@ namespace OC\Files\Mount;
use \OC\Files\Filesystem;
use OC\Files\Storage\StorageFactory;
use OC\Files\Storage\Storage;
+use OC\Files\Storage\Wrapper\Wrapper;
use OCP\Files\Mount\IMountPoint;
class MountPoint implements IMountPoint {
@@ -92,7 +93,11 @@ class MountPoint implements IMountPoint {
$this->mountPoint = $mountpoint;
if ($storage instanceof Storage) {
$this->class = get_class($storage);
- $this->storage = $this->loader->wrap($this, $storage);
+ $this->storage = $storage;
+ // only wrap if not already wrapped
+ if (!($this->storage instanceof Wrapper)) {
+ $this->storage = $this->loader->wrap($this, $this->storage);
+ }
} else {
// Update old classes to new namespace
if (strpos($storage, 'OC_Filestorage_') !== false) {
diff --git a/lib/private/files/objectstore/objectstorestorage.php b/lib/private/files/objectstore/objectstorestorage.php
index 24398649727..a85553186ae 100644
--- a/lib/private/files/objectstore/objectstorestorage.php
+++ b/lib/private/files/objectstore/objectstorestorage.php
@@ -24,6 +24,7 @@
namespace OC\Files\ObjectStore;
+use Icewind\Streams\IteratorDirectory;
use OCP\Files\ObjectStore\IObjectStore;
class ObjectStoreStorage extends \OC\Files\Storage\Common {
@@ -216,9 +217,7 @@ class ObjectStoreStorage extends \OC\Files\Storage\Common {
$files[] = $file['name'];
}
- \OC\Files\Stream\Dir::register('objectstore' . $path . '/', $files);
-
- return opendir('fakedir://objectstore' . $path . '/');
+ return IteratorDirectory::wrap($files);
} catch (\Exception $e) {
\OCP\Util::writeLog('objectstore', $e->getMessage(), \OCP\Util::ERROR);
return false;
diff --git a/lib/private/files/storage/common.php b/lib/private/files/storage/common.php
index 78f35ad4a6f..a5ed5fd3996 100644
--- a/lib/private/files/storage/common.php
+++ b/lib/private/files/storage/common.php
@@ -404,6 +404,11 @@ abstract class Common implements Storage {
return implode('/', $output);
}
+ /**
+ * Test a storage for availability
+ *
+ * @return bool
+ */
public function test() {
if ($this->stat('')) {
return true;
@@ -650,4 +655,18 @@ abstract class Common implements Storage {
public function changeLock($path, $type, ILockingProvider $provider) {
$provider->changeLock('files/' . md5($this->getId() . '::' . trim($path, '/')), $type);
}
+
+ /**
+ * @return array [ available, last_checked ]
+ */
+ public function getAvailability() {
+ return $this->getStorageCache()->getAvailability();
+ }
+
+ /**
+ * @param bool $isAvailable
+ */
+ public function setAvailability($isAvailable) {
+ $this->getStorageCache()->setAvailability($isAvailable);
+ }
}
diff --git a/lib/private/files/storage/dav.php b/lib/private/files/storage/dav.php
index 24cf3c29209..6e89dcccbcd 100644
--- a/lib/private/files/storage/dav.php
+++ b/lib/private/files/storage/dav.php
@@ -38,7 +38,7 @@ namespace OC\Files\Storage;
use Exception;
use OC\Files\Filesystem;
use OC\Files\Stream\Close;
-use OC\Files\Stream\Dir;
+use Icewind\Streams\IteratorDirectory;
use OC\MemCache\ArrayCache;
use OCP\Constants;
use OCP\Files;
@@ -211,8 +211,7 @@ class DAV extends Common {
$file = basename($file);
$content[] = $file;
}
- Dir::register($id, $content);
- return opendir('fakedir://' . $id);
+ return IteratorDirectory::wrap($content);
} catch (ClientHttpException $e) {
if ($e->getHttpStatus() === 404) {
$this->statCache->clear($path . '/');
diff --git a/lib/private/files/storage/local.php b/lib/private/files/storage/local.php
index b7272b7d1f0..3676fe69131 100644
--- a/lib/private/files/storage/local.php
+++ b/lib/private/files/storage/local.php
@@ -35,7 +35,6 @@
*/
namespace OC\Files\Storage;
-
/**
* for local filestore, we only have to map the paths
*/
diff --git a/lib/private/files/storage/wrapper/availability.php b/lib/private/files/storage/wrapper/availability.php
new file mode 100644
index 00000000000..37319a8f7d1
--- /dev/null
+++ b/lib/private/files/storage/wrapper/availability.php
@@ -0,0 +1,462 @@
+<?php
+/**
+ * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+namespace OC\Files\Storage\Wrapper;
+
+/**
+ * Availability checker for storages
+ *
+ * Throws a StorageNotAvailableException for storages with known failures
+ */
+class Availability extends Wrapper {
+ const RECHECK_TTL_SEC = 600; // 10 minutes
+
+ /**
+ * @return bool
+ */
+ private function updateAvailability() {
+ try {
+ $result = $this->test();
+ } catch (\Exception $e) {
+ $result = false;
+ }
+ $this->setAvailability($result);
+ return $result;
+ }
+
+ /**
+ * @return bool
+ */
+ private function isAvailable() {
+ $availability = $this->getAvailability();
+ if (!$availability['available']) {
+ // trigger a recheck if TTL reached
+ if ((time() - $availability['last_checked']) > self::RECHECK_TTL_SEC) {
+ return $this->updateAvailability();
+ }
+ }
+ return $availability['available'];
+ }
+
+ /**
+ * @throws \OCP\Files\StorageNotAvailableException
+ */
+ private function checkAvailability() {
+ if (!$this->isAvailable()) {
+ throw new \OCP\Files\StorageNotAvailableException();
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function mkdir($path) {
+ $this->checkAvailability();
+ try {
+ return parent::mkdir($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function rmdir($path) {
+ $this->checkAvailability();
+ try {
+ return parent::rmdir($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function opendir($path) {
+ $this->checkAvailability();
+ try {
+ return parent::opendir($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function is_dir($path) {
+ $this->checkAvailability();
+ try {
+ return parent::is_dir($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function is_file($path) {
+ $this->checkAvailability();
+ try {
+ return parent::is_file($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function stat($path) {
+ $this->checkAvailability();
+ try {
+ return parent::stat($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function filetype($path) {
+ $this->checkAvailability();
+ try {
+ return parent::filetype($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function filesize($path) {
+ $this->checkAvailability();
+ try {
+ return parent::filesize($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function isCreatable($path) {
+ $this->checkAvailability();
+ try {
+ return parent::isCreatable($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function isReadable($path) {
+ $this->checkAvailability();
+ try {
+ return parent::isReadable($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function isUpdatable($path) {
+ $this->checkAvailability();
+ try {
+ return parent::isUpdatable($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function isDeletable($path) {
+ $this->checkAvailability();
+ try {
+ return parent::isDeletable($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function isSharable($path) {
+ $this->checkAvailability();
+ try {
+ return parent::isSharable($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getPermissions($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getPermissions($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function file_exists($path) {
+ $this->checkAvailability();
+ try {
+ return parent::file_exists($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function filemtime($path) {
+ $this->checkAvailability();
+ try {
+ return parent::filemtime($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function file_get_contents($path) {
+ $this->checkAvailability();
+ try {
+ return parent::file_get_contents($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function file_put_contents($path, $data) {
+ $this->checkAvailability();
+ try {
+ return parent::file_put_contents($path, $data);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function unlink($path) {
+ $this->checkAvailability();
+ try {
+ return parent::unlink($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function rename($path1, $path2) {
+ $this->checkAvailability();
+ try {
+ return parent::rename($path1, $path2);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function copy($path1, $path2) {
+ $this->checkAvailability();
+ try {
+ return parent::copy($path1, $path2);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function fopen($path, $mode) {
+ $this->checkAvailability();
+ try {
+ return parent::fopen($path, $mode);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getMimeType($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getMimeType($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function hash($type, $path, $raw = false) {
+ $this->checkAvailability();
+ try {
+ return parent::hash($type, $path, $raw);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function free_space($path) {
+ $this->checkAvailability();
+ try {
+ return parent::free_space($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function search($query) {
+ $this->checkAvailability();
+ try {
+ return parent::search($query);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function touch($path, $mtime = null) {
+ $this->checkAvailability();
+ try {
+ return parent::touch($path, $mtime);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getLocalFile($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getLocalFile($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getLocalFolder($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getLocalFolder($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function hasUpdated($path, $time) {
+ $this->checkAvailability();
+ try {
+ return parent::hasUpdated($path, $time);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getOwner($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getOwner($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getETag($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getETag($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getDirectDownload($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getDirectDownload($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function copyFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
+ $this->checkAvailability();
+ try {
+ return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function moveFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
+ $this->checkAvailability();
+ try {
+ return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+
+ /** {@inheritdoc} */
+ public function getMetaData($path) {
+ $this->checkAvailability();
+ try {
+ return parent::getMetaData($path);
+ } catch (\OCP\Files\StorageNotAvailableException $e) {
+ $this->setAvailability(false);
+ throw $e;
+ }
+ }
+}
diff --git a/lib/private/files/storage/wrapper/wrapper.php b/lib/private/files/storage/wrapper/wrapper.php
index d1414880beb..b43dd4fe142 100644
--- a/lib/private/files/storage/wrapper/wrapper.php
+++ b/lib/private/files/storage/wrapper/wrapper.php
@@ -498,6 +498,24 @@ class Wrapper implements \OC\Files\Storage\Storage {
}
/**
+ * Get availability of the storage
+ *
+ * @return array [ available, last_checked ]
+ */
+ public function getAvailability() {
+ return $this->storage->getAvailability();
+ }
+
+ /**
+ * Set availability of the storage
+ *
+ * @param bool $isAvailable
+ */
+ public function setAvailability($isAvailable) {
+ $this->storage->setAvailability($isAvailable);
+ }
+
+ /**
* @param string $path the path of the target folder
* @param string $fileName the name of the file itself
* @return void
diff --git a/lib/private/files/view.php b/lib/private/files/view.php
index 1a6be73d5bb..dc6e8aaa719 100644
--- a/lib/private/files/view.php
+++ b/lib/private/files/view.php
@@ -1354,7 +1354,7 @@ class View {
// if sharing was disabled for the user we remove the share permissions
if (\OCP\Util::isSharingDisabledForUser()) {
- $content['permissions'] = $content['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
+ $rootEntry['permissions'] = $rootEntry['permissions'] & ~\OCP\Constants::PERMISSION_SHARE;
}
$files[] = new FileInfo($path . '/' . $rootEntry['name'], $subStorage, '', $rootEntry, $mount);
diff --git a/lib/private/installer.php b/lib/private/installer.php
index 37af8d0edcb..392dc1c0817 100644
--- a/lib/private/installer.php
+++ b/lib/private/installer.php
@@ -107,6 +107,10 @@ class OC_Installer{
}
$extractDir .= '/' . $info['id'];
+ if(!file_exists($extractDir)) {
+ OC_Helper::rmdirr($basedir);
+ throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id']));
+ }
OC_Helper::copyr($extractDir, $basedir);
//remove temporary files
diff --git a/lib/private/ocs.php b/lib/private/ocs.php
index 6d166f8adb0..bb1aabf8f18 100644
--- a/lib/private/ocs.php
+++ b/lib/private/ocs.php
@@ -28,6 +28,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
+use OCP\API;
/**
* Class to handle open collaboration services API requests
@@ -64,8 +65,7 @@ class OC_OCS {
}
}
if ($data === false) {
- echo self::generateXml('', 'fail', 400, 'Bad request. Please provide a valid '.$key);
- exit();
+ throw new \OC\OCS\Exception(new OC_OCS_Result(null, 400, 'Bad request. Please provide a valid '.$key));
} else {
// NOTE: Is the raw type necessary? It might be a little risky without sanitization
if ($type == 'raw') return $data;
@@ -78,23 +78,12 @@ class OC_OCS {
}
public static function notFound() {
- if($_SERVER['REQUEST_METHOD'] == 'GET') {
- $method='get';
- }elseif($_SERVER['REQUEST_METHOD'] == 'PUT') {
- $method='put';
- }elseif($_SERVER['REQUEST_METHOD'] == 'POST') {
- $method='post';
- }else{
- echo('internal server error: method not supported');
- exit();
- }
-
- $format = self::readData($method, 'format', 'text', '');
+ $format = OC_API::requestedFormat();
$txt='Invalid query, please check the syntax. API specifications are here:'
.' http://www.freedesktop.org/wiki/Specifications/open-collaboration-services. DEBUG OUTPUT:'."\n";
$txt.=OC_OCS::getDebugOutput();
- echo(OC_OCS::generateXml($format, 'failed', 999, $txt));
+ OC_API::respond(new OC_OCS_Result(null, API::RESPOND_UNKNOWN_ERROR, $txt), $format);
}
/**
@@ -110,130 +99,4 @@ class OC_OCS {
if(isset($_POST)) foreach($_POST as $key=>$value) $txt.='post parameter: '.$key.'->'.$value."\n";
return($txt);
}
-
-
- /**
- * generates the xml or json response for the API call from an multidimenional data array.
- * @param string $format
- * @param string $status
- * @param string $statuscode
- * @param string $message
- * @param array $data
- * @param string $tag
- * @param string $tagattribute
- * @param int $dimension
- * @param int|string $itemscount
- * @param int|string $itemsperpage
- * @return string xml/json
- */
- public static function generateXml($format, $status, $statuscode,
- $message, $data=array(), $tag='', $tagattribute='', $dimension=-1, $itemscount='', $itemsperpage='') {
- if($format=='json') {
- $json=array();
- $json['status']=$status;
- $json['statuscode']=$statuscode;
- $json['message']=$message;
- $json['totalitems']=$itemscount;
- $json['itemsperpage']=$itemsperpage;
- $json['data']=$data;
- return(json_encode($json));
- }else{
- $txt='';
- $writer = xmlwriter_open_memory();
- xmlwriter_set_indent( $writer, 2 );
- xmlwriter_start_document($writer );
- xmlwriter_start_element($writer, 'ocs');
- xmlwriter_start_element($writer, 'meta');
- xmlwriter_write_element($writer, 'status', $status);
- xmlwriter_write_element($writer, 'statuscode', $statuscode);
- xmlwriter_write_element($writer, 'message', $message);
- if($itemscount<>'') xmlwriter_write_element($writer, 'totalitems', $itemscount);
- if(!empty($itemsperpage)) xmlwriter_write_element($writer, 'itemsperpage', $itemsperpage);
- xmlwriter_end_element($writer);
- if($dimension=='0') {
- // 0 dimensions
- xmlwriter_write_element($writer, 'data', $data);
-
- }elseif($dimension=='1') {
- xmlwriter_start_element($writer, 'data');
- foreach($data as $key=>$entry) {
- xmlwriter_write_element($writer, $key, $entry);
- }
- xmlwriter_end_element($writer);
-
- }elseif($dimension=='2') {
- xmlwriter_start_element($writer, 'data');
- foreach($data as $entry) {
- xmlwriter_start_element($writer, $tag);
- if(!empty($tagattribute)) {
- xmlwriter_write_attribute($writer, 'details', $tagattribute);
- }
- foreach($entry as $key=>$value) {
- if(is_array($value)) {
- foreach($value as $k=>$v) {
- xmlwriter_write_element($writer, $k, $v);
- }
- } else {
- xmlwriter_write_element($writer, $key, $value);
- }
- }
- xmlwriter_end_element($writer);
- }
- xmlwriter_end_element($writer);
-
- }elseif($dimension=='3') {
- xmlwriter_start_element($writer, 'data');
- foreach($data as $entrykey=>$entry) {
- xmlwriter_start_element($writer, $tag);
- if(!empty($tagattribute)) {
- xmlwriter_write_attribute($writer, 'details', $tagattribute);
- }
- foreach($entry as $key=>$value) {
- if(is_array($value)) {
- xmlwriter_start_element($writer, $entrykey);
- foreach($value as $k=>$v) {
- xmlwriter_write_element($writer, $k, $v);
- }
- xmlwriter_end_element($writer);
- } else {
- xmlwriter_write_element($writer, $key, $value);
- }
- }
- xmlwriter_end_element($writer);
- }
- xmlwriter_end_element($writer);
- }elseif($dimension=='dynamic') {
- xmlwriter_start_element($writer, 'data');
- OC_OCS::toxml($writer, $data, 'comment');
- xmlwriter_end_element($writer);
- }
-
- xmlwriter_end_element($writer);
-
- xmlwriter_end_document( $writer );
- $txt.=xmlwriter_output_memory( $writer );
- unset($writer);
- return($txt);
- }
- }
-
- /**
- * @param resource $writer
- * @param array $data
- * @param string $node
- */
- public static function toXml($writer, $data, $node) {
- foreach($data as $key => $value) {
- if (is_numeric($key)) {
- $key = $node;
- }
- if (is_array($value)) {
- xmlwriter_start_element($writer, $key);
- OC_OCS::toxml($writer, $value, $node);
- xmlwriter_end_element($writer);
- }else{
- xmlwriter_write_element($writer, $key, $value);
- }
- }
- }
}
diff --git a/lib/private/ocs/cloud.php b/lib/private/ocs/cloud.php
index f662bde2858..8f4f1769e9c 100644
--- a/lib/private/ocs/cloud.php
+++ b/lib/private/ocs/cloud.php
@@ -3,6 +3,7 @@
* @author Bart Visscher <bartv@thisnet.nl>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tom Needham <tom@owncloud.com>
*
@@ -36,12 +37,8 @@ class OC_OCS_Cloud {
'edition' => OC_Util::getEditionString(),
);
- $result['capabilities'] = array(
- 'core' => array(
- 'pollinterval' => OC_Config::getValue('pollinterval', 60),
- ),
- );
-
+ $result['capabilities'] = \OC::$server->getCapabilitiesManager()->getCapabilities();
+
return new OC_OCS_Result($result);
}
diff --git a/lib/private/ocs/corecapabilities.php b/lib/private/ocs/corecapabilities.php
new file mode 100644
index 00000000000..6b620ab0a84
--- /dev/null
+++ b/lib/private/ocs/corecapabilities.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\OCS;
+
+use OCP\Capabilities\ICapability;
+use OCP\IConfig;
+
+/**
+ * Class Capabilities
+ *
+ * @package OC\OCS
+ */
+class CoreCapabilities implements ICapability {
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * @param IConfig $config
+ */
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+ /**
+ * Return this classes capabilities
+ *
+ * @return array
+ */
+ public function getCapabilities() {
+ return [
+ 'core' => [
+ 'pollinterval' => $this->config->getSystemValue('pollinterval', 60)
+ ]
+ ];
+ }
+}
diff --git a/lib/private/ocs/exception.php b/lib/private/ocs/exception.php
new file mode 100644
index 00000000000..93bee773771
--- /dev/null
+++ b/lib/private/ocs/exception.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\OCS;
+
+class Exception extends \Exception {
+
+ public function __construct(\OC_OCS_Result $result) {
+ $this->result = $result;
+ }
+
+ public function getResult() {
+ return $this->result;
+ }
+
+}
diff --git a/lib/private/ocs/result.php b/lib/private/ocs/result.php
index 1ee2982ac4a..c4b0fbf33f3 100644
--- a/lib/private/ocs/result.php
+++ b/lib/private/ocs/result.php
@@ -27,7 +27,23 @@
class OC_OCS_Result{
- protected $data, $message, $statusCode, $items, $perPage;
+ /** @var array */
+ protected $data;
+
+ /** @var null|string */
+ protected $message;
+
+ /** @var int */
+ protected $statusCode;
+
+ /** @var integer */
+ protected $items;
+
+ /** @var integer */
+ protected $perPage;
+
+ /** @var array */
+ private $headers = [];
/**
* create the OCS_Result object
@@ -106,5 +122,32 @@ class OC_OCS_Result{
return ($this->statusCode == 100);
}
+ /**
+ * Adds a new header to the response
+ * @param string $name The name of the HTTP header
+ * @param string $value The value, null will delete it
+ * @return $this
+ */
+ public function addHeader($name, $value) {
+ $name = trim($name); // always remove leading and trailing whitespace
+ // to be able to reliably check for security
+ // headers
+
+ if(is_null($value)) {
+ unset($this->headers[$name]);
+ } else {
+ $this->headers[$name] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the set headers
+ * @return array the headers
+ */
+ public function getHeaders() {
+ return $this->headers;
+ }
}
diff --git a/lib/private/route/router.php b/lib/private/route/router.php
index 48992366092..33669452f2d 100644
--- a/lib/private/route/router.php
+++ b/lib/private/route/router.php
@@ -150,6 +150,12 @@ class Router implements IRouter {
\OC::$server->getEventLogger()->start('loadroutes' . $requestedApp, 'Loading Routes');
foreach ($routingFiles as $app => $file) {
if (!isset($this->loadedApps[$app])) {
+ if (!\OC_App::isAppLoaded($app)) {
+ // app MUST be loaded before app routes
+ // try again next time loadRoutes() is called
+ $this->loaded = false;
+ continue;
+ }
$this->loadedApps[$app] = true;
$this->useCollection($app);
$this->requireRouteFile($file, $app);
diff --git a/lib/private/server.php b/lib/private/server.php
index 12981fe7f19..9503cf16ff7 100644
--- a/lib/private/server.php
+++ b/lib/private/server.php
@@ -449,6 +449,14 @@ class Server extends SimpleContainer implements IServerContainer {
$c->getURLGenerator(),
\OC::$configDir);
});
+ $this->registerService('CapabilitiesManager', function (Server $c) {
+ $manager = new \OC\CapabilitiesManager();
+ $manager->registerCapability(function() use ($c) {
+ return new \OC\OCS\CoreCapabilities($c->getConfig());
+ });
+ return $manager;
+ });
+
}
/**
@@ -945,4 +953,13 @@ class Server extends SimpleContainer implements IServerContainer {
public function getMimeTypeDetector() {
return $this->query('MimeTypeDetector');
}
+
+ /**
+ * Get the manager of all the capabilities
+ *
+ * @return \OC\CapabilitiesManager
+ */
+ public function getCapabilitiesManager() {
+ return $this->query('CapabilitiesManager');
+ }
}
diff --git a/lib/private/share/share.php b/lib/private/share/share.php
index 40fcc59f219..9aea4677b5b 100644
--- a/lib/private/share/share.php
+++ b/lib/private/share/share.php
@@ -1218,7 +1218,7 @@ class Share extends Constants {
$qb = $connection->getQueryBuilder();
$qb->select('uid_owner')
- ->from('*PREFIX*share')
+ ->from('share')
->where($qb->expr()->eq('id', $qb->createParameter('shareId')))
->setParameter(':shareId', $shareId);
$result = $qb->execute();
@@ -1269,7 +1269,7 @@ class Share extends Constants {
self::verifyPassword($password);
$qb = $connection->getQueryBuilder();
- $qb->update('*PREFIX*share')
+ $qb->update('share')
->set('share_with', $qb->createParameter('pass'))
->where($qb->expr()->eq('id', $qb->createParameter('shareId')))
->setParameter(':pass', is_null($password) ? null : \OC::$server->getHasher()->hash($password))
diff --git a/lib/private/util.php b/lib/private/util.php
index 1b22e03ca6f..501dbf5c4c5 100644
--- a/lib/private/util.php
+++ b/lib/private/util.php
@@ -143,6 +143,14 @@ class OC_Util {
return $storage;
});
+ // install storage availability wrapper, before most other wrappers
+ \OC\Files\Filesystem::addStorageWrapper('oc_availability', function ($mountPoint, $storage) {
+ if (!$storage->isLocal()) {
+ return new \OC\Files\Storage\Wrapper\Availability(['storage' => $storage]);
+ }
+ return $storage;
+ });
+
\OC\Files\Filesystem::addStorageWrapper('oc_quota', function ($mountPoint, $storage) {
// set up quota for home storages, even for other users
// which can happen when using sharing
diff --git a/lib/public/appframework/http/ocsresponse.php b/lib/public/appframework/http/ocsresponse.php
index 52d3c2fa665..2e788a52bb9 100644
--- a/lib/public/appframework/http/ocsresponse.php
+++ b/lib/public/appframework/http/ocsresponse.php
@@ -41,38 +41,26 @@ class OCSResponse extends Response {
private $format;
private $statuscode;
private $message;
- private $tag;
- private $tagattribute;
- private $dimension;
private $itemscount;
private $itemsperpage;
/**
* generates the xml or json response for the API call from an multidimenional data array.
* @param string $format
- * @param string $status
- * @param string $statuscode
+ * @param int $statuscode
* @param string $message
* @param array $data
- * @param string $tag
- * @param string $tagattribute
- * @param int $dimension
* @param int|string $itemscount
* @param int|string $itemsperpage
* @since 8.1.0
*/
- public function __construct($format, $status, $statuscode, $message,
- $data=[], $tag='', $tagattribute='',
- $dimension=-1, $itemscount='',
+ public function __construct($format, $statuscode, $message,
+ $data=[], $itemscount='',
$itemsperpage='') {
$this->format = $format;
- $this->setStatus($status);
$this->statuscode = $statuscode;
$this->message = $message;
$this->data = $data;
- $this->tag = $tag;
- $this->tagattribute = $tagattribute;
- $this->dimension = $dimension;
$this->itemscount = $itemscount;
$this->itemsperpage = $itemsperpage;
@@ -93,11 +81,11 @@ class OCSResponse extends Response {
* @since 8.1.0
*/
public function render() {
- return OC_OCS::generateXml(
- $this->format, $this->getStatus(), $this->statuscode, $this->message,
- $this->data, $this->tag, $this->tagattribute, $this->dimension,
- $this->itemscount, $this->itemsperpage
- );
+ $r = new \OC_OCS_Result($this->data, $this->statuscode, $this->message);
+ $r->setTotalItems($this->itemscount);
+ $r->setItemsPerPage($this->itemsperpage);
+
+ return \OC_API::renderResult($r, $this->format);
}
diff --git a/lib/public/appframework/iappcontainer.php b/lib/public/appframework/iappcontainer.php
index 64b1082aa97..7338dbd858d 100644
--- a/lib/public/appframework/iappcontainer.php
+++ b/lib/public/appframework/iappcontainer.php
@@ -86,4 +86,11 @@ interface IAppContainer extends IContainer {
*/
function log($message, $level);
+ /**
+ * Register a capability
+ *
+ * @param string $serviceName e.g. 'OCA\Files\Capabilities'
+ * @since 8.2.0
+ */
+ public function registerCapability($serviceName);
}
diff --git a/lib/public/appframework/ocscontroller.php b/lib/public/appframework/ocscontroller.php
index 602731fe761..55ba518020a 100644
--- a/lib/public/appframework/ocscontroller.php
+++ b/lib/public/appframework/ocscontroller.php
@@ -42,7 +42,7 @@ abstract class OCSController extends ApiController {
* constructor of the controller
* @param string $appName the name of the app
* @param IRequest $request an instance of the request
- * @param string $corsMethods comma seperated string of HTTP verbs which
+ * @param string $corsMethods comma separated string of HTTP verbs which
* should be allowed for websites or webapps when calling your API, defaults to
* 'PUT, POST, GET, DELETE, PATCH'
* @param string $corsAllowedHeaders comma seperated string of HTTP headers
@@ -80,13 +80,9 @@ abstract class OCSController extends ApiController {
}
$params = [
- 'status' => 'OK',
'statuscode' => 100,
'message' => 'OK',
'data' => [],
- 'tag' => '',
- 'tagattribute' => '',
- 'dimension' => 'dynamic',
'itemscount' => '',
'itemsperpage' => ''
];
@@ -96,9 +92,8 @@ abstract class OCSController extends ApiController {
}
return new OCSResponse(
- $format, $params['status'], $params['statuscode'],
- $params['message'], $params['data'], $params['tag'],
- $params['tagattribute'], $params['dimension'],
+ $format, $params['statuscode'],
+ $params['message'], $params['data'],
$params['itemscount'], $params['itemsperpage']
);
}
diff --git a/lib/public/capabilities/icapability.php b/lib/public/capabilities/icapability.php
new file mode 100644
index 00000000000..71a56128d26
--- /dev/null
+++ b/lib/public/capabilities/icapability.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCP\Capabilities;
+
+/**
+ * Minimal interface that has to be implemented for a class to be considered
+ * a capability.
+ *
+ * In an application use:
+ * $this->getContainer()->registerCapability('OCA\MY_APP\Capabilities');
+ * To register capabilities.
+ *
+ * The class 'OCA\MY_APP\Capabilities' must then implement ICapability
+ *
+ * @since 8.2.0
+ */
+interface ICapability {
+
+ /**
+ * Function an app uses to return the capabilities
+ *
+ * @return array Array containing the apps capabilities
+ * @since 8.2.0
+ */
+ public function getCapabilities();
+}
+
diff --git a/lib/public/db/querybuilder/iquerybuilder.php b/lib/public/db/querybuilder/iquerybuilder.php
index 09d5d199bef..3fc07af1a47 100644
--- a/lib/public/db/querybuilder/iquerybuilder.php
+++ b/lib/public/db/querybuilder/iquerybuilder.php
@@ -27,6 +27,15 @@ namespace OCP\DB\QueryBuilder;
*/
interface IQueryBuilder {
/**
+ * Enable/disable automatic prefixing of table names with the oc_ prefix
+ *
+ * @param bool $enabled If set to true table names will be prefixed with the
+ * owncloud database prefix automatically.
+ * @since 8.2.0
+ */
+ public function automaticTablePrefix($enabled);
+
+ /**
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
* This producer method is intended for convenient inline usage. Example:
*
diff --git a/lib/public/files/storage.php b/lib/public/files/storage.php
index 41218996382..ac3603e48d4 100644
--- a/lib/public/files/storage.php
+++ b/lib/public/files/storage.php
@@ -439,4 +439,24 @@ interface Storage {
* @since 8.1.0
*/
public function changeLock($path, $type, ILockingProvider $provider);
+
+ /**
+ * Test a storage for availability
+ *
+ * @since 8.2.0
+ * @return bool
+ */
+ public function test();
+
+ /**
+ * @since 8.2.0
+ * @return array [ available, last_checked ]
+ */
+ public function getAvailability();
+
+ /**
+ * @since 8.2.0
+ * @param bool $isAvailable
+ */
+ public function setAvailability($isAvailable);
}
diff --git a/lib/public/iservercontainer.php b/lib/public/iservercontainer.php
index f3165db33da..ab1729da255 100644
--- a/lib/public/iservercontainer.php
+++ b/lib/public/iservercontainer.php
@@ -438,4 +438,5 @@ interface IServerContainer {
* @since 8.2.0
*/
public function getMimeTypeDetector();
+
}
diff --git a/lib/repair/cleantags.php b/lib/repair/cleantags.php
index 2bda1047081..d16a49fbca7 100644
--- a/lib/repair/cleantags.php
+++ b/lib/repair/cleantags.php
@@ -65,8 +65,8 @@ class CleanTags extends BasicEmitter implements RepairStep {
protected function deleteOrphanFileEntries() {
$this->deleteOrphanEntries(
'%d tags for delete files have been removed.',
- '*PREFIX*vcategory_to_object', 'objid',
- '*PREFIX*filecache', 'fileid', 'path_hash'
+ 'vcategory_to_object', 'objid',
+ 'filecache', 'fileid', 'path_hash'
);
}
@@ -76,8 +76,8 @@ class CleanTags extends BasicEmitter implements RepairStep {
protected function deleteOrphanTagEntries() {
$this->deleteOrphanEntries(
'%d tag entries for deleted tags have been removed.',
- '*PREFIX*vcategory_to_object', 'categoryid',
- '*PREFIX*vcategory', 'id', 'uid'
+ 'vcategory_to_object', 'categoryid',
+ 'vcategory', 'id', 'uid'
);
}
@@ -87,8 +87,8 @@ class CleanTags extends BasicEmitter implements RepairStep {
protected function deleteOrphanCategoryEntries() {
$this->deleteOrphanEntries(
'%d tags with no entries have been removed.',
- '*PREFIX*vcategory', 'id',
- '*PREFIX*vcategory_to_object', 'categoryid', 'type'
+ 'vcategory', 'id',
+ 'vcategory_to_object', 'categoryid', 'type'
);
}
diff --git a/lib/repair/filletags.php b/lib/repair/filletags.php
index f1bb2c896c4..40072209982 100644
--- a/lib/repair/filletags.php
+++ b/lib/repair/filletags.php
@@ -42,7 +42,7 @@ class FillETags extends BasicEmitter implements \OC\RepairStep {
public function run() {
$qb = $this->connection->getQueryBuilder();
- $qb->update('*PREFIX*filecache')
+ $qb->update('filecache')
->set('etag', $qb->expr()->literal('xxx'))
->where($qb->expr()->eq('etag', $qb->expr()->literal('')))
->orWhere($qb->expr()->isNull('etag'));
diff --git a/ocs/v1.php b/ocs/v1.php
index 2829cf08c57..c5c18d20b8a 100644
--- a/ocs/v1.php
+++ b/ocs/v1.php
@@ -56,5 +56,7 @@ try {
} catch (MethodNotAllowedException $e) {
OC_API::setContentType();
OC_Response::setStatus(405);
+} catch (\OC\OCS\Exception $ex) {
+ OC_API::respond($ex->getResult(), OC_API::requestedFormat());
}
diff --git a/ocs/v2.php b/ocs/v2.php
new file mode 100644
index 00000000000..b2e3b259727
--- /dev/null
+++ b/ocs/v2.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+require_once 'v1.php';
diff --git a/settings/application.php b/settings/application.php
index 8da835c18d2..155cc39d041 100644
--- a/settings/application.php
+++ b/settings/application.php
@@ -107,7 +107,8 @@ class Application extends App {
$c->query('AppName'),
$c->query('Request'),
$c->query('CertificateManager'),
- $c->query('L10N')
+ $c->query('L10N'),
+ $c->query('IAppManager')
);
});
$container->registerService('GroupsController', function(IContainer $c) {
diff --git a/settings/controller/certificatecontroller.php b/settings/controller/certificatecontroller.php
index ea20b7c587f..92d0961efb7 100644
--- a/settings/controller/certificatecontroller.php
+++ b/settings/controller/certificatecontroller.php
@@ -21,6 +21,7 @@
namespace OC\Settings\Controller;
+use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
@@ -36,20 +37,25 @@ class CertificateController extends Controller {
private $certificateManager;
/** @var IL10N */
private $l10n;
+ /** @var IAppManager */
+ private $appManager;
/**
* @param string $appName
* @param IRequest $request
* @param ICertificateManager $certificateManager
* @param IL10N $l10n
+ * @param IAppManager $appManager
*/
public function __construct($appName,
IRequest $request,
ICertificateManager $certificateManager,
- IL10N $l10n) {
+ IL10N $l10n,
+ IAppManager $appManager) {
parent::__construct($appName, $request);
$this->certificateManager = $certificateManager;
$this->l10n = $l10n;
+ $this->appManager = $appManager;
}
/**
@@ -60,6 +66,11 @@ class CertificateController extends Controller {
* @return array
*/
public function addPersonalRootCertificate() {
+
+ if ($this->isCertificateImportAllowed() === false) {
+ return new DataResponse('Individual certificate management disabled', Http::STATUS_FORBIDDEN);
+ }
+
$file = $this->request->getUploadedFile('rootcert_import');
if(empty($file)) {
return new DataResponse(['message' => 'No file uploaded'], Http::STATUS_UNPROCESSABLE_ENTITY);
@@ -92,8 +103,29 @@ class CertificateController extends Controller {
* @return DataResponse
*/
public function removePersonalRootCertificate($certificateIdentifier) {
+
+ if ($this->isCertificateImportAllowed() === false) {
+ return new DataResponse('Individual certificate management disabled', Http::STATUS_FORBIDDEN);
+ }
+
$this->certificateManager->removeCertificate($certificateIdentifier);
return new DataResponse();
}
+ /**
+ * check if certificate import is allowed
+ *
+ * @return bool
+ */
+ protected function isCertificateImportAllowed() {
+ $externalStorageEnabled = $this->appManager->isEnabledForUser('files_external');
+ if ($externalStorageEnabled) {
+ $backends = \OC_Mount_Config::getPersonalBackends();
+ if (!empty($backends)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/settings/controller/checksetupcontroller.php b/settings/controller/checksetupcontroller.php
index f849e3ed565..ff605b474e2 100644
--- a/settings/controller/checksetupcontroller.php
+++ b/settings/controller/checksetupcontroller.php
@@ -175,6 +175,23 @@ class CheckSetupController extends Controller {
return '';
}
+
+ /*
+ * Whether the php version is still supported (at time of release)
+ * according to: https://secure.php.net/supported-versions.php
+ *
+ * @return array
+ */
+ private function isPhpSupported() {
+ $eol = false;
+
+ //PHP 5.4 is EOL on 14 Sep 2015
+ if (version_compare(PHP_VERSION, '5.5.0') === -1) {
+ $eol = true;
+ }
+
+ return ['eol' => $eol, 'version' => PHP_VERSION];
+ }
/**
* @return DataResponse
@@ -189,6 +206,7 @@ class CheckSetupController extends Controller {
'isUrandomAvailable' => $this->isUrandomAvailable(),
'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'),
'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(),
+ 'phpSupported' => $this->isPhpSupported(),
]
);
}
diff --git a/settings/css/settings.css b/settings/css/settings.css
index e0fe9b446be..0af63821627 100644
--- a/settings/css/settings.css
+++ b/settings/css/settings.css
@@ -386,6 +386,12 @@ table.grid td.date{
display: inline-block;
}
+#encryptionAPI li {
+ list-style-type: initial;
+ margin-left: 20px;
+ padding: 5px 0;
+}
+
.mail_settings p label:first-child {
display: inline-block;
width: 300px;
diff --git a/settings/js/admin.js b/settings/js/admin.js
index 8f705b9048d..3e17d7cc182 100644
--- a/settings/js/admin.js
+++ b/settings/js/admin.js
@@ -85,6 +85,13 @@ $(document).ready(function(){
});
});
+ $('#shareapiExpireAfterNDays').change(function() {
+ var value = $(this).val();
+ if (value <= 0) {
+ $(this).val("1");
+ }
+ });
+
$('#shareAPI input:not(#excludedGroups)').change(function() {
var value = $(this).val();
if ($(this).attr('type') === 'checkbox') {
diff --git a/settings/js/users/filter.js b/settings/js/users/filter.js
index 72f2cfc6d24..339d6ad5ec7 100644
--- a/settings/js/users/filter.js
+++ b/settings/js/users/filter.js
@@ -7,16 +7,13 @@
/**
* @brief this object takes care of the filter functionality on the user
* management page
- * @param jQuery input element that works as the user text input field
- * @param object the UserList object
+ * @param {UserList} userList the UserList object
+ * @param {GroupList} groupList the GroupList object
*/
-function UserManagementFilter(filterInput, userList, groupList) {
- this.filterInput = filterInput;
+function UserManagementFilter (userList, groupList) {
this.userList = userList;
this.groupList = groupList;
- this.filterGroups = false;
- this.thread = undefined;
- this.oldval = this.filterInput.val();
+ this.oldFilter = '';
this.init();
}
@@ -24,39 +21,23 @@ function UserManagementFilter(filterInput, userList, groupList) {
/**
* @brief sets up when the filter action shall be triggered
*/
-UserManagementFilter.prototype.init = function() {
- var umf = this;
- this.filterInput.keyup(function(e) {
- //we want to react on any printable letter, plus on modifying stuff like
- //Backspace and Delete. extended https://stackoverflow.com/a/12467610
- var valid =
- e.keyCode === 0 || e.keyCode === 8 || // like ö or ж; backspace
- e.keyCode === 9 || e.keyCode === 46 || // tab; delete
- e.keyCode === 32 || // space
- (e.keyCode > 47 && e.keyCode < 58) || // number keys
- (e.keyCode > 64 && e.keyCode < 91) || // letter keys
- (e.keyCode > 95 && e.keyCode < 112) || // numpad keys
- (e.keyCode > 185 && e.keyCode < 193) || // ;=,-./` (in order)
- (e.keyCode > 218 && e.keyCode < 223); // [\]' (in order)
-
- //besides the keys, the value must have been changed compared to last
- //time
- if(valid && umf.oldVal !== umf.getPattern()) {
- umf.run();
- }
-
- umf.oldVal = umf.getPattern();
- });
+UserManagementFilter.prototype.init = function () {
+ OC.Plugins.register('OCA.Search', this);
};
/**
* @brief the filter action needs to be done, here the accurate steps are being
* taken care of
*/
-UserManagementFilter.prototype.run = _.debounce(function() {
+UserManagementFilter.prototype.run = _.debounce(function (filter) {
+ if (filter === this.oldFilter) {
+ return;
+ }
+ this.oldFilter = filter;
+ this.userList.filter = filter;
this.userList.empty();
this.userList.update(GroupList.getCurrentGID());
- if(this.filterGroups) {
+ if (this.groupList.filterGroups) {
// user counts are being updated nevertheless
this.groupList.empty();
}
@@ -69,12 +50,12 @@ UserManagementFilter.prototype.run = _.debounce(function() {
* @brief returns the filter String
* @returns string
*/
-UserManagementFilter.prototype.getPattern = function() {
+UserManagementFilter.prototype.getPattern = function () {
var input = this.filterInput.val(),
html = $('html'),
isIE8or9 = html.hasClass('lte9');
// FIXME - TODO - once support for IE8 and IE9 is dropped
- if(isIE8or9 && input == this.filterInput.attr('placeholder')) {
+ if (isIE8or9 && input == this.filterInput.attr('placeholder')) {
input = '';
}
return input;
@@ -84,10 +65,14 @@ UserManagementFilter.prototype.getPattern = function() {
* @brief adds reset functionality to an HTML element
* @param jQuery the jQuery representation of that element
*/
-UserManagementFilter.prototype.addResetButton = function(button) {
+UserManagementFilter.prototype.addResetButton = function (button) {
var umf = this;
- button.click(function(){
+ button.click(function () {
umf.filterInput.val('');
umf.run();
});
};
+
+UserManagementFilter.prototype.attach = function (search) {
+ search.setFilter('settings', this.run.bind(this));
+};
diff --git a/settings/js/users/groups.js b/settings/js/users/groups.js
index d205e915508..322db6c1b45 100644
--- a/settings/js/users/groups.js
+++ b/settings/js/users/groups.js
@@ -12,6 +12,8 @@ var GroupList;
GroupList = {
activeGID: '',
everyoneGID: '_everyone',
+ filter: '',
+ filterGroups: false,
addGroup: function (gid, usercount) {
var $li = $userGroupList.find('.isgroup:last-child').clone();
@@ -145,8 +147,8 @@ GroupList = {
$.get(
OC.generateUrl('/settings/users/groups'),
{
- pattern: filter.getPattern(),
- filterGroups: filter.filterGroups ? 1 : 0,
+ pattern: this.filter,
+ filterGroups: this.filterGroups ? 1 : 0,
sortGroups: $sortGroupBy
},
function (result) {
diff --git a/settings/js/users/users.js b/settings/js/users/users.js
index 3b25bcd5b5f..6f29d6fe25b 100644
--- a/settings/js/users/users.js
+++ b/settings/js/users/users.js
@@ -8,13 +8,13 @@
var $userList;
var $userListBody;
-var filter;
var UserList = {
availableGroups: [],
offset: 0,
usersToLoad: 10, //So many users will be loaded when user scrolls down
currentGid: '',
+ filter: '',
/**
* Initializes the user list
@@ -229,7 +229,7 @@ var UserList = {
return aa.length - bb.length;
},
preSortSearchString: function(a, b) {
- var pattern = filter.getPattern();
+ var pattern = this.filter;
if(typeof pattern === 'undefined') {
return undefined;
}
@@ -398,7 +398,7 @@ var UserList = {
gid = '';
}
UserList.currentGid = gid;
- var pattern = filter.getPattern();
+ var pattern = this.filter;
$.get(
OC.generateUrl('/settings/users/users'),
{ offset: UserList.offset, limit: limit, gid: gid, pattern: pattern },
@@ -612,7 +612,7 @@ $(document).ready(function () {
UserList.initDeleteHandling();
// Implements User Search
- filter = new UserManagementFilter($('#usersearchform input'), UserList, GroupList);
+ OCA.Search.users= new UserManagementFilter(UserList, GroupList);
UserList.doSort();
UserList.availableGroups = $userList.data('groups');
diff --git a/settings/personal.php b/settings/personal.php
index 8823102e01a..203c9f68af8 100644
--- a/settings/personal.php
+++ b/settings/personal.php
@@ -104,6 +104,17 @@ $clients = array(
'ios' => $config->getSystemValue('customclient_ios', $defaults->getiOSClientUrl())
);
+// only show root certificate import if external storages are enabled
+$enableCertImport = false;
+$externalStorageEnabled = \OC::$server->getAppManager()->isEnabledForUser('files_external');
+if ($externalStorageEnabled) {
+ $backends = OC_Mount_Config::getPersonalBackends();
+ if (!empty($backends)) {
+ $enableCertImport = true;
+ }
+}
+
+
// Return template
$tmpl = new OC_Template( 'settings', 'personal', 'user');
$tmpl->assign('usage', OC_Helper::humanFileSize($storageInfo['used']));
@@ -120,6 +131,7 @@ $tmpl->assign('displayName', OC_User::getDisplayName());
$tmpl->assign('enableAvatars', $config->getSystemValue('enable_avatars', true));
$tmpl->assign('avatarChangeSupported', OC_User::canUserChangeAvatar(OC_User::getUser()));
$tmpl->assign('certs', $certificateManager->listCertificates());
+$tmpl->assign('showCertificates', $enableCertImport);
$tmpl->assign('urlGenerator', $urlGenerator);
// Get array of group ids for this user
@@ -157,7 +169,11 @@ $formsMap = array_map(function($form){
$formsAndMore = array_merge($formsAndMore, $formsMap);
// add bottom hardcoded forms from the template
-$formsAndMore[]= array( 'anchor' => 'ssl-root-certificates', 'section-name' => $l->t('SSL root certificates') );
+if($enableCertImport) {
+ $formsAndMore[]= array( 'anchor' => 'ssl-root-certificates', 'section-name' => $l->t('SSL root certificates') );
+}
+
+
$tmpl->assign('forms', $formsAndMore);
$tmpl->printPage();
diff --git a/settings/templates/admin.php b/settings/templates/admin.php
index 4203ee2cad7..ff8a2f0c953 100644
--- a/settings/templates/admin.php
+++ b/settings/templates/admin.php
@@ -326,10 +326,17 @@ if ($_['cronErrors']) {
</p>
<div id="EncryptionWarning" class="warning hidden">
- <?php p($l->t('Encryption is a one way process. Once encryption is enabled, all files from that point forward will be encrypted on the server and it will not be possible to disable encryption at a later date. This is the final warning: Do you really want to enable encryption?')) ?>
- <input type="button"
+ <p><?php p($l->t('Please read carefully before activating server-side encryption: ')); ?></p>
+ <ul>
+ <li><?php p($l->t('Server-side encryption is a one way process. Once encryption is enabled, all files from that point forward will be encrypted on the server and it will not be possible to disable encryption at a later date')); ?></li>
+ <li><?php p($l->t('Anyone who has privileged access to your ownCloud server can decrypt your files either by intercepting requests or reading out user passwords which are stored in plain text session files. Server-side encryption does therefore not protect against malicious administrators but is useful for protecting your data on externally hosted storage.')); ?></li>
+ <li><?php p($l->t('Depending on the actual encryption module the general file size is increased (by 35%% or more when using the default module)')); ?></li>
+ <li><?php p($l->t('You should regularly backup all encryption keys to prevent permanent data loss (data/<user>/files_encryption and data/files_encryption)')); ?></li>
+ </ul>
+
+ <p><?php p($l->t('This is the final warning: Do you really want to enable encryption?')) ?> <input type="button"
id="reallyEnableEncryption"
- value="<?php p($l->t("Enable encryption")); ?>" />
+ value="<?php p($l->t("Enable encryption")); ?>" /></p>
</div>
<div id="EncryptionSettingsArea" class="<?php if (!$_['encryptionEnabled']) p('hidden'); ?>">
diff --git a/settings/templates/personal.php b/settings/templates/personal.php
index e7832b85ebd..490133c9f25 100644
--- a/settings/templates/personal.php
+++ b/settings/templates/personal.php
@@ -205,6 +205,7 @@ if($_['passwordChangeSupported']) {
<?php }
};?>
+<?php if($_['showCertificates']) : ?>
<div id="ssl-root-certificates" class="section">
<h2><?php p($l->t('SSL root certificates')); ?></h2>
<table id="sslCertificate" class="grid">
@@ -242,6 +243,7 @@ if($_['passwordChangeSupported']) {
<input type="button" id="rootcert_import_button" value="<?php p($l->t('Import root certificate')); ?>"/>
</form>
</div>
+<?php endif; ?>
<div class="section">
<h2><?php p($l->t('Version'));?></h2>
diff --git a/settings/templates/users/part.createuser.php b/settings/templates/users/part.createuser.php
index 9d9886f694c..0fc5a2bdeaa 100644
--- a/settings/templates/users/part.createuser.php
+++ b/settings/templates/users/part.createuser.php
@@ -31,7 +31,4 @@
alt="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"/>
</div>
<?php endif; ?>
- <form autocomplete="off" id="usersearchform">
- <input type="text" class="input userFilter" placeholder="<?php p($l->t('Search Users')); ?>" />
- </form>
</div>
diff --git a/tests/lib/appframework/controller/OCSControllerTest.php b/tests/lib/appframework/controller/OCSControllerTest.php
index 11a9d45eb92..92b092cf0e9 100644
--- a/tests/lib/appframework/controller/OCSControllerTest.php
+++ b/tests/lib/appframework/controller/OCSControllerTest.php
@@ -69,9 +69,11 @@ class OCSControllerTest extends \Test\TestCase {
$expected = "<?xml version=\"1.0\"?>\n" .
"<ocs>\n" .
" <meta>\n" .
- " <status>OK</status>\n" .
+ " <status>failure</status>\n" .
" <statuscode>400</statuscode>\n" .
" <message>OK</message>\n" .
+ " <totalitems></totalitems>\n" .
+ " <itemsperpage></itemsperpage>\n" .
" </meta>\n" .
" <data>\n" .
" <test>hi</test>\n" .
@@ -99,9 +101,11 @@ class OCSControllerTest extends \Test\TestCase {
$expected = "<?xml version=\"1.0\"?>\n" .
"<ocs>\n" .
" <meta>\n" .
- " <status>OK</status>\n" .
+ " <status>failure</status>\n" .
" <statuscode>400</statuscode>\n" .
" <message>OK</message>\n" .
+ " <totalitems></totalitems>\n" .
+ " <itemsperpage></itemsperpage>\n" .
" </meta>\n" .
" <data>\n" .
" <test>hi</test>\n" .
@@ -126,8 +130,8 @@ class OCSControllerTest extends \Test\TestCase {
$this->getMock('\OCP\Security\ISecureRandom'),
$this->getMock('\OCP\IConfig')
));
- $expected = '{"status":"OK","statuscode":400,"message":"OK",' .
- '"totalitems":"","itemsperpage":"","data":{"test":"hi"}}';
+ $expected = '{"ocs":{"meta":{"status":"failure","statuscode":400,"message":"OK",' .
+ '"totalitems":"","itemsperpage":""},"data":{"test":"hi"}}}';
$params = [
'data' => [
'test' => 'hi'
diff --git a/tests/lib/appframework/http/OCSResponseTest.php b/tests/lib/appframework/http/OCSResponseTest.php
index 111dc7ad0a3..1ca3e330bad 100644
--- a/tests/lib/appframework/http/OCSResponseTest.php
+++ b/tests/lib/appframework/http/OCSResponseTest.php
@@ -47,14 +47,13 @@ class OCSResponseTest extends \Test\TestCase {
public function testRender() {
$response = new OCSResponse(
- 'xml', 'status', 2, 'message', ['test' => 'hi'], 'tag', 'abc',
- 'dynamic', 3, 4
+ 'xml', 2, 'message', ['test' => 'hi'], 3, 4
);
$out = $response->render();
$expected = "<?xml version=\"1.0\"?>\n" .
"<ocs>\n" .
" <meta>\n" .
- " <status>status</status>\n" .
+ " <status>failure</status>\n" .
" <statuscode>2</statuscode>\n" .
" <message>message</message>\n" .
" <totalitems>3</totalitems>\n" .
diff --git a/tests/lib/capabilitiesmanager.php b/tests/lib/capabilitiesmanager.php
new file mode 100644
index 00000000000..b5dac80ee51
--- /dev/null
+++ b/tests/lib/capabilitiesmanager.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace Test;
+
+use OC\CapabilitiesManager;
+
+class CapabilitiesManagerTest extends TestCase {
+
+ /**
+ * Test no capabilities
+ */
+ public function testNoCapabilities() {
+ $manager = new \OC\CapabilitiesManager();
+ $res = $manager->getCapabilities();
+ $this->assertEmpty($res);
+ }
+
+ /**
+ * Test a valid capabilitie
+ */
+ public function testValidCapability() {
+ $manager = new \OC\CapabilitiesManager();
+
+ $manager->registerCapability(function() {
+ return new SimpleCapability();
+ });
+
+ $res = $manager->getCapabilities();
+ $this->assertEquals(['foo' => 1], $res);
+ }
+
+ /**
+ * Test that we need something that implents ICapability
+ * @expectedException \InvalidArgumentException
+ * @expectedExceptionMessage The given Capability (Test\NoCapability) does not implement the ICapability interface
+ */
+ public function testNoICapability() {
+ $manager = new \OC\CapabilitiesManager();
+
+ $manager->registerCapability(function() {
+ return new NoCapability();
+ });
+
+ $res = $manager->getCapabilities();
+ $this->assertEquals([], $res);
+ }
+
+ /**
+ * Test a bunch of merged Capabilities
+ */
+ public function testMergedCapabilities() {
+ $manager = new \OC\CapabilitiesManager();
+
+ $manager->registerCapability(function() {
+ return new SimpleCapability();
+ });
+ $manager->registerCapability(function() {
+ return new SimpleCapability2();
+ });
+ $manager->registerCapability(function() {
+ return new SimpleCapability3();
+ });
+
+ $res = $manager->getCapabilities();
+ $expected = [
+ 'foo' => 1,
+ 'bar' => [
+ 'x' => 1,
+ 'y' => 2
+ ]
+ ];
+
+ $this->assertEquals($expected, $res);
+ }
+
+ /**
+ * Test deep identical capabilities
+ */
+ public function testDeepIdenticalCapabilities() {
+ $manager = new \OC\CapabilitiesManager();
+
+ $manager->registerCapability(function() {
+ return new DeepCapability();
+ });
+ $manager->registerCapability(function() {
+ return new DeepCapability();
+ });
+
+ $res = $manager->getCapabilities();
+ $expected = [
+ 'foo' => [
+ 'bar' => [
+ 'baz' => true
+ ]
+ ]
+ ];
+
+ $this->assertEquals($expected, $res);
+ }
+}
+
+class SimpleCapability implements \OCP\Capabilities\ICapability {
+ public function getCapabilities() {
+ return [
+ 'foo' => 1
+ ];
+ }
+}
+
+class SimpleCapability2 implements \OCP\Capabilities\ICapability {
+ public function getCapabilities() {
+ return [
+ 'bar' => ['x' => 1]
+ ];
+ }
+}
+
+class SimpleCapability3 implements \OCP\Capabilities\ICapability {
+ public function getCapabilities() {
+ return [
+ 'bar' => ['y' => 2]
+ ];
+ }
+}
+
+class NoCapability {
+ public function getCapabilities() {
+ return [
+ 'baz' => 'z'
+ ];
+ }
+}
+
+class DeepCapability implements \OCP\Capabilities\ICapability {
+ public function getCapabilities() {
+ return [
+ 'foo' => [
+ 'bar' => [
+ 'baz' => true
+ ]
+ ]
+ ];
+ }
+}
+
diff --git a/tests/lib/db/querybuilder/querybuildertest.php b/tests/lib/db/querybuilder/querybuildertest.php
index 02e516b7386..75e62ba944e 100644
--- a/tests/lib/db/querybuilder/querybuildertest.php
+++ b/tests/lib/db/querybuilder/querybuildertest.php
@@ -253,8 +253,8 @@ class QueryBuilderTest extends \Test\TestCase {
public function dataDelete() {
return [
- ['data', null, ['table' => '`data`', 'alias' => null], '`data`'],
- ['data', 't', ['table' => '`data`', 'alias' => 't'], '`data` t'],
+ ['data', null, ['table' => '`*PREFIX*data`', 'alias' => null], '`*PREFIX*data`'],
+ ['data', 't', ['table' => '`*PREFIX*data`', 'alias' => 't'], '`*PREFIX*data` t'],
];
}
@@ -282,8 +282,8 @@ class QueryBuilderTest extends \Test\TestCase {
public function dataUpdate() {
return [
- ['data', null, ['table' => '`data`', 'alias' => null], '`data`'],
- ['data', 't', ['table' => '`data`', 'alias' => 't'], '`data` t'],
+ ['data', null, ['table' => '`*PREFIX*data`', 'alias' => null], '`*PREFIX*data`'],
+ ['data', 't', ['table' => '`*PREFIX*data`', 'alias' => 't'], '`*PREFIX*data` t'],
];
}
@@ -311,7 +311,7 @@ class QueryBuilderTest extends \Test\TestCase {
public function dataInsert() {
return [
- ['data', ['table' => '`data`'], '`data`'],
+ ['data', ['table' => '`*PREFIX*data`'], '`*PREFIX*data`'],
];
}
@@ -338,16 +338,16 @@ class QueryBuilderTest extends \Test\TestCase {
public function dataFrom() {
return [
- ['data', null, null, null, [['table' => '`data`', 'alias' => null]], '`data`'],
- ['data', 't', null, null, [['table' => '`data`', 'alias' => 't']], '`data` t'],
+ ['data', null, null, null, [['table' => '`*PREFIX*data`', 'alias' => null]], '`*PREFIX*data`'],
+ ['data', 't', null, null, [['table' => '`*PREFIX*data`', 'alias' => 't']], '`*PREFIX*data` t'],
['data1', null, 'data2', null, [
- ['table' => '`data1`', 'alias' => null],
- ['table' => '`data2`', 'alias' => null]
- ], '`data1`, `data2`'],
+ ['table' => '`*PREFIX*data1`', 'alias' => null],
+ ['table' => '`*PREFIX*data2`', 'alias' => null]
+ ], '`*PREFIX*data1`, `*PREFIX*data2`'],
['data', 't1', 'data', 't2', [
- ['table' => '`data`', 'alias' => 't1'],
- ['table' => '`data`', 'alias' => 't2']
- ], '`data` t1, `data` t2'],
+ ['table' => '`*PREFIX*data`', 'alias' => 't1'],
+ ['table' => '`*PREFIX*data`', 'alias' => 't2']
+ ], '`*PREFIX*data` t1, `*PREFIX*data` t2'],
];
}
@@ -382,18 +382,18 @@ class QueryBuilderTest extends \Test\TestCase {
return [
[
'd1', 'data2', null, null,
- ['d1' => [['joinType' => 'inner', 'joinTable' => '`data2`', 'joinAlias' => null, 'joinCondition' => null]]],
- '`data1` d1 INNER JOIN `data2` ON '
+ ['d1' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => null, 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 INNER JOIN `*PREFIX*data2` ON '
],
[
'd1', 'data2', 'd2', null,
- ['d1' => [['joinType' => 'inner', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
- '`data1` d1 INNER JOIN `data2` d2 ON '
+ ['d1' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 INNER JOIN `*PREFIX*data2` d2 ON '
],
[
'd1', 'data2', 'd2', 'd1.`field1` = d2.`field2`',
- ['d1' => [['joinType' => 'inner', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
- '`data1` d1 INNER JOIN `data2` d2 ON d1.`field1` = d2.`field2`'
+ ['d1' => [['joinType' => 'inner', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
+ '`*PREFIX*data1` d1 INNER JOIN `*PREFIX*data2` d2 ON d1.`field1` = d2.`field2`'
],
];
@@ -463,18 +463,18 @@ class QueryBuilderTest extends \Test\TestCase {
return [
[
'd1', 'data2', null, null,
- ['d1' => [['joinType' => 'left', 'joinTable' => '`data2`', 'joinAlias' => null, 'joinCondition' => null]]],
- '`data1` d1 LEFT JOIN `data2` ON '
+ ['d1' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => null, 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 LEFT JOIN `*PREFIX*data2` ON '
],
[
'd1', 'data2', 'd2', null,
- ['d1' => [['joinType' => 'left', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
- '`data1` d1 LEFT JOIN `data2` d2 ON '
+ ['d1' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 LEFT JOIN `*PREFIX*data2` d2 ON '
],
[
'd1', 'data2', 'd2', 'd1.`field1` = d2.`field2`',
- ['d1' => [['joinType' => 'left', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
- '`data1` d1 LEFT JOIN `data2` d2 ON d1.`field1` = d2.`field2`'
+ ['d1' => [['joinType' => 'left', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
+ '`*PREFIX*data1` d1 LEFT JOIN `*PREFIX*data2` d2 ON d1.`field1` = d2.`field2`'
],
];
}
@@ -513,18 +513,18 @@ class QueryBuilderTest extends \Test\TestCase {
return [
[
'd1', 'data2', null, null,
- ['d1' => [['joinType' => 'right', 'joinTable' => '`data2`', 'joinAlias' => null, 'joinCondition' => null]]],
- '`data1` d1 RIGHT JOIN `data2` ON '
+ ['d1' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => null, 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 RIGHT JOIN `*PREFIX*data2` ON '
],
[
'd1', 'data2', 'd2', null,
- ['d1' => [['joinType' => 'right', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
- '`data1` d1 RIGHT JOIN `data2` d2 ON '
+ ['d1' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => null]]],
+ '`*PREFIX*data1` d1 RIGHT JOIN `*PREFIX*data2` d2 ON '
],
[
'd1', 'data2', 'd2', 'd1.`field1` = d2.`field2`',
- ['d1' => [['joinType' => 'right', 'joinTable' => '`data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
- '`data1` d1 RIGHT JOIN `data2` d2 ON d1.`field1` = d2.`field2`'
+ ['d1' => [['joinType' => 'right', 'joinTable' => '`*PREFIX*data2`', 'joinAlias' => 'd2', 'joinCondition' => 'd1.`field1` = d2.`field2`']]],
+ '`*PREFIX*data1` d1 RIGHT JOIN `*PREFIX*data2` d2 ON d1.`field1` = d2.`field2`'
],
];
}
@@ -591,7 +591,7 @@ class QueryBuilderTest extends \Test\TestCase {
);
$this->assertSame(
- 'UPDATE `data` SET ' . $expectedQuery,
+ 'UPDATE `*PREFIX*data` SET ' . $expectedQuery,
$this->queryBuilder->getSQL()
);
}
@@ -774,7 +774,7 @@ class QueryBuilderTest extends \Test\TestCase {
);
$this->assertSame(
- 'INSERT INTO `data` ' . $expectedQuery,
+ 'INSERT INTO `*PREFIX*data` ' . $expectedQuery,
$this->queryBuilder->getSQL()
);
}
@@ -799,7 +799,7 @@ class QueryBuilderTest extends \Test\TestCase {
);
$this->assertSame(
- 'INSERT INTO `data` ' . $expectedQuery,
+ 'INSERT INTO `*PREFIX*data` ' . $expectedQuery,
$this->queryBuilder->getSQL()
);
}
@@ -996,4 +996,34 @@ class QueryBuilderTest extends \Test\TestCase {
$this->queryBuilder->getSQL()
);
}
+
+ public function dataGetTableName() {
+ return [
+ ['*PREFIX*table', null, '`*PREFIX*table`'],
+ ['*PREFIX*table', true, '`*PREFIX*table`'],
+ ['*PREFIX*table', false, '`*PREFIX*table`'],
+
+ ['table', null, '`*PREFIX*table`'],
+ ['table', true, '`*PREFIX*table`'],
+ ['table', false, '`table`'],
+ ];
+ }
+
+ /**
+ * @dataProvider dataGetTableName
+ *
+ * @param string $tableName
+ * @param bool $automatic
+ * @param string $expected
+ */
+ public function testGetTableName($tableName, $automatic, $expected) {
+ if ($automatic !== null) {
+ $this->queryBuilder->automaticTablePrefix($automatic);
+ }
+
+ $this->assertSame(
+ $expected,
+ $this->invokePrivate($this->queryBuilder, 'getTableName', [$tableName])
+ );
+ }
}
diff --git a/tests/lib/files/mount/mountpoint.php b/tests/lib/files/mount/mountpoint.php
index 29610e6058d..d758c1b8d4d 100644
--- a/tests/lib/files/mount/mountpoint.php
+++ b/tests/lib/files/mount/mountpoint.php
@@ -70,4 +70,25 @@ class MountPoint extends \Test\TestCase {
// storage wrapper never called
$this->assertFalse($called);
}
+
+ public function testWrappedStorage() {
+ $storage = $this->getMockBuilder('\OC\Files\Storage\Wrapper\Wrapper')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $loader = $this->getMock('\OCP\Files\Storage\IStorageFactory');
+ $loader->expects($this->never())
+ ->method('getInstance');
+ $loader->expects($this->never())
+ ->method('wrap');
+
+ $mountPoint = new \OC\Files\Mount\MountPoint(
+ $storage,
+ '/mountpoint',
+ null,
+ $loader
+ );
+
+ $this->assertEquals($storage, $mountPoint->getStorage());
+ }
}
diff --git a/tests/lib/files/storage/wrapper/availability.php b/tests/lib/files/storage/wrapper/availability.php
new file mode 100644
index 00000000000..9b394df8ca3
--- /dev/null
+++ b/tests/lib/files/storage/wrapper/availability.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+namespace Test\Files\Storage\Wrapper;
+
+class Availability extends \Test\TestCase {
+ protected function getWrapperInstance() {
+ $storage = $this->getMockBuilder('\OC\Files\Storage\Temporary')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $wrapper = new \OC\Files\Storage\Wrapper\Availability(['storage' => $storage]);
+ return [$storage, $wrapper];
+ }
+
+ /**
+ * Storage is available
+ */
+ public function testAvailable() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => true, 'last_checked' => 0]);
+ $storage->expects($this->never())
+ ->method('test');
+ $storage->expects($this->once())
+ ->method('mkdir');
+
+ $wrapper->mkdir('foobar');
+ }
+
+ /**
+ * Storage marked unavailable, TTL not expired
+ *
+ * @expectedException \OCP\Files\StorageNotAvailableException
+ */
+ public function testUnavailable() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => false, 'last_checked' => time()]);
+ $storage->expects($this->never())
+ ->method('test');
+ $storage->expects($this->never())
+ ->method('mkdir');
+
+ $wrapper->mkdir('foobar');
+ }
+
+ /**
+ * Storage marked unavailable, TTL expired
+ */
+ public function testUnavailableRecheck() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => false, 'last_checked' => 0]);
+ $storage->expects($this->once())
+ ->method('test')
+ ->willReturn(true);
+ $storage->expects($this->once())
+ ->method('setAvailability')
+ ->with($this->equalTo(true));
+ $storage->expects($this->once())
+ ->method('mkdir');
+
+ $wrapper->mkdir('foobar');
+ }
+
+ /**
+ * Storage marked available, but throws StorageNotAvailableException
+ *
+ * @expectedException \OCP\Files\StorageNotAvailableException
+ */
+ public function testAvailableThrowStorageNotAvailable() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => true, 'last_checked' => 0]);
+ $storage->expects($this->never())
+ ->method('test');
+ $storage->expects($this->once())
+ ->method('mkdir')
+ ->will($this->throwException(new \OCP\Files\StorageNotAvailableException()));
+ $storage->expects($this->once())
+ ->method('setAvailability')
+ ->with($this->equalTo(false));
+
+ $wrapper->mkdir('foobar');
+ }
+
+ /**
+ * Storage available, but call fails
+ * Method failure does not indicate storage unavailability
+ */
+ public function testAvailableFailure() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => true, 'last_checked' => 0]);
+ $storage->expects($this->never())
+ ->method('test');
+ $storage->expects($this->once())
+ ->method('mkdir')
+ ->willReturn(false);
+ $storage->expects($this->never())
+ ->method('setAvailability');
+
+ $wrapper->mkdir('foobar');
+ }
+
+ /**
+ * Storage available, but throws exception
+ * Standard exception does not indicate storage unavailability
+ *
+ * @expectedException \Exception
+ */
+ public function testAvailableThrow() {
+ list($storage, $wrapper) = $this->getWrapperInstance();
+ $storage->expects($this->once())
+ ->method('getAvailability')
+ ->willReturn(['available' => true, 'last_checked' => 0]);
+ $storage->expects($this->never())
+ ->method('test');
+ $storage->expects($this->once())
+ ->method('mkdir')
+ ->will($this->throwException(new \Exception()));
+ $storage->expects($this->never())
+ ->method('setAvailability');
+
+ $wrapper->mkdir('foobar');
+ }
+}
diff --git a/tests/lib/files/view.php b/tests/lib/files/view.php
index 382c033f19c..bf99a582117 100644
--- a/tests/lib/files/view.php
+++ b/tests/lib/files/view.php
@@ -44,8 +44,22 @@ class View extends \Test\TestCase {
* @var \OC\Files\Storage\Storage[] $storages
*/
private $storages = array();
+
+ /**
+ * @var string
+ */
private $user;
+ /**
+ * @var \OCP\IUser
+ */
+ private $userObject;
+
+ /**
+ * @var \OCP\IGroup
+ */
+ private $groupObject;
+
/** @var \OC\Files\Storage\Storage */
private $tempStorage;
@@ -57,10 +71,15 @@ class View extends \Test\TestCase {
\OC_User::useBackend(new \OC_User_Dummy());
//login
- \OC_User::createUser('test', 'test');
+ $userManager = \OC::$server->getUserManager();
+ $groupManager = \OC::$server->getGroupManager();
+ $this->user = 'test';
+ $this->userObject = $userManager->createUser('test', 'test');
- $this->loginAsUser('test');
- $this->user = \OC_User::getUser();
+ $this->groupObject = $groupManager->createGroup('group1');
+ $this->groupObject->addUser($this->userObject);
+
+ $this->loginAsUser($this->user);
// clear mounts but somehow keep the root storage
// that was initialized above...
\OC\Files\Filesystem::clearMounts();
@@ -81,6 +100,10 @@ class View extends \Test\TestCase {
}
$this->logout();
+
+ $this->userObject->delete();
+ $this->groupObject->delete();
+
parent::tearDown();
}
@@ -208,6 +231,40 @@ class View extends \Test\TestCase {
$this->assertEquals(4, count($folderContent));
}
+ public function sharingDisabledPermissionProvider() {
+ return [
+ ['no', '', true],
+ ['yes', 'group1', false],
+ ];
+ }
+
+ /**
+ * @dataProvider sharingDisabledPermissionProvider
+ */
+ public function testRemoveSharePermissionWhenSharingDisabledForUser($excludeGroups, $excludeGroupsList, $expectedShareable) {
+ $appConfig = \OC::$server->getAppConfig();
+ $oldExcludeGroupsFlag = $appConfig->getValue('core', 'shareapi_exclude_groups', 'no');
+ $oldExcludeGroupsList = $appConfig->getValue('core', 'shareapi_exclude_groups_list', '');
+ $appConfig->setValue('core', 'shareapi_exclude_groups', $excludeGroups);
+ $appConfig->setValue('core', 'shareapi_exclude_groups_list', $excludeGroupsList);
+
+ $storage1 = $this->getTestStorage();
+ $storage2 = $this->getTestStorage();
+ \OC\Files\Filesystem::mount($storage1, array(), '/');
+ \OC\Files\Filesystem::mount($storage2, array(), '/mount');
+
+ $view = new \OC\Files\View('/');
+
+ $folderContent = $view->getDirectoryContent('');
+ $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
+
+ $folderContent = $view->getDirectoryContent('mount');
+ $this->assertEquals($expectedShareable, $folderContent[0]->isShareable());
+
+ $appConfig->setValue('core', 'shareapi_exclude_groups', $oldExcludeGroupsFlag);
+ $appConfig->setValue('core', 'shareapi_exclude_groups_list', $oldExcludeGroupsList);
+ }
+
function testCacheIncompleteFolder() {
$storage1 = $this->getTestStorage(false);
\OC\Files\Filesystem::clearMounts();
diff --git a/tests/lib/repair/cleantags.php b/tests/lib/repair/cleantags.php
index 2f6d0879642..a511daa03d6 100644
--- a/tests/lib/repair/cleantags.php
+++ b/tests/lib/repair/cleantags.php
@@ -40,13 +40,13 @@ class CleanTags extends \Test\TestCase {
protected function cleanUpTables() {
$qb = $this->connection->getQueryBuilder();
- $qb->delete('*PREFIX*vcategory')
+ $qb->delete('vcategory')
->execute();
- $qb->delete('*PREFIX*vcategory_to_object')
+ $qb->delete('vcategory_to_object')
->execute();
- $qb->delete('*PREFIX*filecache')
+ $qb->delete('filecache')
->execute();
}
@@ -61,20 +61,20 @@ class CleanTags extends \Test\TestCase {
$this->addTagEntry(9999999, $cat3, 'contacts'); // Retained
$this->addTagEntry($this->getFileID(), $cat3 + 1, 'files'); // Deleted: Category is NULL
- $this->assertEntryCount('*PREFIX*vcategory_to_object', 4, 'Assert tag entries count before repair step');
- $this->assertEntryCount('*PREFIX*vcategory', 4, 'Assert tag categories count before repair step');
+ $this->assertEntryCount('vcategory_to_object', 4, 'Assert tag entries count before repair step');
+ $this->assertEntryCount('vcategory', 4, 'Assert tag categories count before repair step');
self::invokePrivate($this->repair, 'deleteOrphanFileEntries');
- $this->assertEntryCount('*PREFIX*vcategory_to_object', 3, 'Assert tag entries count after cleaning file entries');
- $this->assertEntryCount('*PREFIX*vcategory', 4, 'Assert tag categories count after cleaning file entries');
+ $this->assertEntryCount('vcategory_to_object', 3, 'Assert tag entries count after cleaning file entries');
+ $this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning file entries');
self::invokePrivate($this->repair, 'deleteOrphanTagEntries');
- $this->assertEntryCount('*PREFIX*vcategory_to_object', 2, 'Assert tag entries count after cleaning tag entries');
- $this->assertEntryCount('*PREFIX*vcategory', 4, 'Assert tag categories count after cleaning tag entries');
+ $this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning tag entries');
+ $this->assertEntryCount('vcategory', 4, 'Assert tag categories count after cleaning tag entries');
self::invokePrivate($this->repair, 'deleteOrphanCategoryEntries');
- $this->assertEntryCount('*PREFIX*vcategory_to_object', 2, 'Assert tag entries count after cleaning category entries');
- $this->assertEntryCount('*PREFIX*vcategory', 2, 'Assert tag categories count after cleaning category entries');
+ $this->assertEntryCount('vcategory_to_object', 2, 'Assert tag entries count after cleaning category entries');
+ $this->assertEntryCount('vcategory', 2, 'Assert tag categories count after cleaning category entries');
}
/**
@@ -100,7 +100,7 @@ class CleanTags extends \Test\TestCase {
*/
protected function addTagCategory($category, $type) {
$qb = $this->connection->getQueryBuilder();
- $qb->insert('*PREFIX*vcategory')
+ $qb->insert('vcategory')
->values([
'uid' => $qb->createNamedParameter('TestRepairCleanTags'),
'category' => $qb->createNamedParameter($category),
@@ -108,7 +108,7 @@ class CleanTags extends \Test\TestCase {
])
->execute();
- return (int) $this->getLastInsertID('*PREFIX*vcategory', 'id');
+ return (int) $this->getLastInsertID('vcategory', 'id');
}
/**
@@ -119,7 +119,7 @@ class CleanTags extends \Test\TestCase {
*/
protected function addTagEntry($objectId, $category, $type) {
$qb = $this->connection->getQueryBuilder();
- $qb->insert('*PREFIX*vcategory_to_object')
+ $qb->insert('vcategory_to_object')
->values([
'objid' => $qb->createNamedParameter($objectId, \PDO::PARAM_INT),
'categoryid' => $qb->createNamedParameter($category, \PDO::PARAM_INT),
@@ -141,21 +141,21 @@ class CleanTags extends \Test\TestCase {
// We create a new file entry and delete it after the test again
$fileName = $this->getUniqueID('TestRepairCleanTags', 12);
- $qb->insert('*PREFIX*filecache')
+ $qb->insert('filecache')
->values([
'path' => $qb->createNamedParameter($fileName),
'path_hash' => $qb->createNamedParameter(md5($fileName)),
])
->execute();
$fileName = $this->getUniqueID('TestRepairCleanTags', 12);
- $qb->insert('*PREFIX*filecache')
+ $qb->insert('filecache')
->values([
'path' => $qb->createNamedParameter($fileName),
'path_hash' => $qb->createNamedParameter(md5($fileName)),
])
->execute();
- $this->createdFile = (int) $this->getLastInsertID('*PREFIX*filecache', 'fileid');
+ $this->createdFile = (int) $this->getLastInsertID('filecache', 'fileid');
return $this->createdFile;
}
diff --git a/tests/lib/server.php b/tests/lib/server.php
index cf0ad8265bf..9c5c83ceb5c 100644
--- a/tests/lib/server.php
+++ b/tests/lib/server.php
@@ -1,6 +1,9 @@
<?php
/**
* @author Joas Schilling <nickvergessen@owncloud.com>
+ * @author Lukas Reschke <lukas@owncloud.com>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
@@ -48,6 +51,7 @@ class Server extends \Test\TestCase {
['AvatarManager', '\OC\AvatarManager'],
['AvatarManager', '\OCP\IAvatarManager'],
+ ['CapabilitiesManager', '\OC\CapabilitiesManager'],
['ContactsManager', '\OC\ContactsManager'],
['ContactsManager', '\OCP\Contacts\IManager'],
['Crypto', '\OC\Security\Crypto'],
diff --git a/tests/lib/share/share.php b/tests/lib/share/share.php
index b6d3e16826d..ef0d9822085 100644
--- a/tests/lib/share/share.php
+++ b/tests/lib/share/share.php
@@ -1288,7 +1288,7 @@ class Test_Share extends \Test\TestCase {
// Find the share ID in the db
$qb = $connection->getQueryBuilder();
$qb->select('id')
- ->from('*PREFIX*share')
+ ->from('share')
->where($qb->expr()->eq('item_type', $qb->createParameter('type')))
->andWhere($qb->expr()->eq('item_source', $qb->createParameter('source')))
->andWhere($qb->expr()->eq('uid_owner', $qb->createParameter('owner')))
@@ -1309,7 +1309,7 @@ class Test_Share extends \Test\TestCase {
// Fetch the hash from the database
$qb = $connection->getQueryBuilder();
$qb->select('share_with')
- ->from('*PREFIX*share')
+ ->from('share')
->where($qb->expr()->eq('id', $qb->createParameter('id')))
->setParameter('id', $id);
$hash = $qb->execute()->fetch()['share_with'];
diff --git a/tests/ocs/response.php b/tests/ocs/response.php
new file mode 100644
index 00000000000..919915a7c78
--- /dev/null
+++ b/tests/ocs/response.php
@@ -0,0 +1,42 @@
+<?php
+use OCP\AppFramework\Http;
+
+/**
+ * @author Thomas Müller <thomas.mueller@tmit.eu>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see <http://www.gnu.org/licenses/>
+ *
+ */
+
+class OcsResponseTest extends \Test\TestCase {
+
+ /**
+ * @dataProvider providesStatusCodes
+ */
+ public function testStatusCodeMapper($expected, $sc) {
+ $result = OC_API::mapStatusCodes($sc);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function providesStatusCodes() {
+ return [
+ [Http::STATUS_OK, 100],
+ [Http::STATUS_BAD_REQUEST, 104],
+ [Http::STATUS_BAD_REQUEST, 1000],
+ [201, 201],
+ ];
+ }
+}
diff --git a/tests/settings/controller/CertificateControllerTest.php b/tests/settings/controller/CertificateControllerTest.php
index b6981195034..023d7753cca 100644
--- a/tests/settings/controller/CertificateControllerTest.php
+++ b/tests/settings/controller/CertificateControllerTest.php
@@ -21,6 +21,7 @@
namespace OC\Settings\Controller;
+use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
@@ -41,6 +42,8 @@ class CertificateControllerTest extends \Test\TestCase {
private $certificateManager;
/** @var IL10N */
private $l10n;
+ /** @var IAppManager */
+ private $appManager;
public function setUp() {
parent::setUp();
@@ -48,13 +51,21 @@ class CertificateControllerTest extends \Test\TestCase {
$this->request = $this->getMock('\OCP\IRequest');
$this->certificateManager = $this->getMock('\OCP\ICertificateManager');
$this->l10n = $this->getMock('\OCP\IL10N');
-
- $this->certificateController = new CertificateController(
- 'settings',
- $this->request,
- $this->certificateManager,
- $this->l10n
- );
+ $this->appManager = $this->getMock('OCP\App\IAppManager');
+
+ $this->certificateController = $this->getMockBuilder('OC\Settings\Controller\CertificateController')
+ ->setConstructorArgs(
+ [
+ 'settings',
+ $this->request,
+ $this->certificateManager,
+ $this->l10n,
+ $this->appManager
+ ]
+ )->setMethods(['isCertificateImportAllowed'])->getMock();
+
+ $this->certificateController->expects($this->any())
+ ->method('isCertificateImportAllowed')->willReturn(true);
}
public function testAddPersonalRootCertificateWithEmptyFile() {
diff --git a/tests/settings/controller/CheckSetupControllerTest.php b/tests/settings/controller/CheckSetupControllerTest.php
index 6096aae8652..62fedd6dd6d 100644
--- a/tests/settings/controller/CheckSetupControllerTest.php
+++ b/tests/settings/controller/CheckSetupControllerTest.php
@@ -31,11 +31,24 @@ use OC_Util;
use Test\TestCase;
/**
+ * Mock version_compare
+ * @param string $version1
+ * @param string $version2
+ * @return int
+ */
+function version_compare($version1, $version2) {
+ return CheckSetupControllerTest::$version_compare;
+}
+
+/**
* Class CheckSetupControllerTest
*
* @package OC\Settings\Controller
*/
class CheckSetupControllerTest extends TestCase {
+ /** @var int */
+ public static $version_compare;
+
/** @var CheckSetupController */
private $checkSetupController;
/** @var IRequest */
@@ -209,6 +222,33 @@ class CheckSetupControllerTest extends TestCase {
);
}
+ public function testIsPhpSupportedFalse() {
+ self::$version_compare = -1;
+
+ $this->assertEquals(
+ ['eol' => true, 'version' => PHP_VERSION],
+ self::invokePrivate($this->checkSetupController, 'isPhpSupported')
+ );
+ }
+
+ public function testIsPhpSupportedTrue() {
+ self::$version_compare = 0;
+
+ $this->assertEquals(
+ ['eol' => false, 'version' => PHP_VERSION],
+ self::invokePrivate($this->checkSetupController, 'isPhpSupported')
+ );
+
+
+ self::$version_compare = 1;
+
+ $this->assertEquals(
+ ['eol' => false, 'version' => PHP_VERSION],
+ self::invokePrivate($this->checkSetupController, 'isPhpSupported')
+ );
+
+ }
+
public function testCheck() {
$this->config->expects($this->at(0))
->method('getSystemValue')
@@ -244,6 +284,7 @@ class CheckSetupControllerTest extends TestCase {
->method('linkToDocs')
->with('admin-security')
->willReturn('https://doc.owncloud.org/server/8.1/admin_manual/configuration_server/hardening.html');
+ self::$version_compare = -1;
$expected = new DataResponse(
[
@@ -254,6 +295,10 @@ class CheckSetupControllerTest extends TestCase {
'isUrandomAvailable' => self::invokePrivate($this->checkSetupController, 'isUrandomAvailable'),
'securityDocs' => 'https://doc.owncloud.org/server/8.1/admin_manual/configuration_server/hardening.html',
'isUsedTlsLibOutdated' => '',
+ 'phpSupported' => [
+ 'eol' => true,
+ 'version' => PHP_VERSION
+ ]
]
);
$this->assertEquals($expected, $this->checkSetupController->check());
diff --git a/themes/example/defaults.php b/themes/example/defaults.php
index 0dd0d46bd9c..21d80416e12 100644
--- a/themes/example/defaults.php
+++ b/themes/example/defaults.php
@@ -28,6 +28,7 @@ class OC_Theme {
private $themeSyncClientUrl;
private $themeSlogan;
private $themeMailHeaderColor;
+ private $themeKnowledgeBaseUrl;
/* put your custom text in these variables */
function __construct() {
@@ -39,6 +40,7 @@ class OC_Theme {
$this->themeSyncClientUrl = 'https://owncloud.org/install';
$this->themeSlogan = 'Your custom cloud, personalized for you!';
$this->themeMailHeaderColor = '#745bca';
+ $this->themeKnowledgeBaseUrl = 'https://doc.owncloud.org';
}
/* nothing after this needs to be adjusted */
@@ -92,4 +94,8 @@ class OC_Theme {
return $this->themeMailHeaderColor;
}
+ public function getKnowledgeBaseUrl() {
+ return $this->themeKnowledgeBaseUrl;
+ }
+
}