Own Cloud Developer Manual
Own Cloud Developer Manual
Own Cloud Developer Manual
Release 8.1
CONTENTS
Table of Contents
1.1 General Contributor Guidelines . . .
1.2 Changelog . . . . . . . . . . . . . .
1.3 Tutorial . . . . . . . . . . . . . . . .
1.4 Create an app . . . . . . . . . . . . .
1.5 Navigation and Pre-App configuration
1.6 App Metadata . . . . . . . . . . . .
1.7 Classloader . . . . . . . . . . . . . .
1.8 Request lifecycle . . . . . . . . . . .
1.9 Routing . . . . . . . . . . . . . . . .
1.10 Middleware . . . . . . . . . . . . . .
1.11 Container . . . . . . . . . . . . . . .
1.12 Controllers . . . . . . . . . . . . . .
1.13 RESTful API . . . . . . . . . . . . .
1.14 Templates . . . . . . . . . . . . . . .
1.15 JavaScript . . . . . . . . . . . . . . .
1.16 CSS . . . . . . . . . . . . . . . . . .
1.17 Translation . . . . . . . . . . . . . .
1.18 Database Schema . . . . . . . . . . .
1.19 Database Access . . . . . . . . . . .
1.20 Configuration . . . . . . . . . . . . .
1.21 Filesystem . . . . . . . . . . . . . .
1.22 Usermanagement . . . . . . . . . . .
1.23 Hooks . . . . . . . . . . . . . . . . .
1.24 Background Jobs (Cron) . . . . . . .
1.25 Logging . . . . . . . . . . . . . . . .
1.26 Testing . . . . . . . . . . . . . . . .
1.27 App Development . . . . . . . . . .
1.28 Android Application Development .
1.29 iOS Application Development . . . .
1.30 Translation . . . . . . . . . . . . . .
1.31 Unit-Testing . . . . . . . . . . . . .
1.32 Theming ownCloud . . . . . . . . .
1.33 Creating and activating a new theme .
1.34 Structure . . . . . . . . . . . . . . .
1.35 How to change images and the logo .
1.36 Testing the new theme out . . . . . .
1.37 Notes for Updates . . . . . . . . . .
1.38 App config . . . . . . . . . . . . . .
1.39 External API . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
23
25
49
49
50
55
56
57
62
65
72
85
87
88
89
97
99
100
104
107
109
111
115
115
117
118
120
129
149
151
154
155
155
155
156
156
157
159
1.40
1.41
1.42
1.43
1.44
ii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
161
164
164
166
175
Translation Translate
ownCloud
into your language
CONTENTS
CONTENTS
CHAPTER
ONE
TABLE OF CONTENTS
lists, IRC channels, forums, etc., will exercise the right to suspend access to any person who persistently breaks our
shared Code of Conduct.
Be considerate
Your actions and work will affect and be used by other people and you in turn will depend on the work and actions of
others. Any decision you take will affect other community members, and we expect you to take those consequences
into account when making decisions.
As a contributor, ensure that you give full credit for the work of others and bear in mind how your changes affect
others. It is also expected that you try to follow the development schedule and guidelines.
As a user, remember that contributors work hard on their part of ownCloud and take great pride in it. If you are
frustrated your problems are more likely to be resolved if you can give accurate and well-mannered information to all
concerned.
Be respectful
In order for the ownCloud community to stay healthy its members must feel comfortable and accepted. Treating one
another with respect is absolutely necessary for this. In a disagreement, in the first instance assume that people mean
well.
We do not tolerate personal attacks, racism, sexism or any other form of discrimination. Disagreement is inevitable,
from time to time, but respect for the views of others will go a long way to winning respect for your own view.
Respecting other people, their work, their contributions and assuming well-meaning motivation will make community
members feel comfortable and safe and will result in motivation and productivity.
We expect members of our community to be respectful when dealing with other contributors, users and communities.
Remember that ownCloud is an international project and that you may be unaware of important aspects of other
cultures.
Be collaborative
The Free Software Movement depends on collaboration: it helps limit duplication of effort while improving the quality
of the software produced. In order to avoid misunderstanding, try to be clear and concise when requesting help or
giving it. Remember it is easy to misunderstand emails (especially when they are not written in your mother tongue).
Ask for clarifications if unsure how something is meant; remember the first rule assume in the first instance that
people mean well.
As a contributor, you should aim to collaborate with other community members, as well as with other communities that
are interested in or depend on the work you do. Your work should be transparent and be fed back into the community
when available, not just when ownCloud releases. If you wish to work on something new in existing projects, keep
those projects informed of your ideas and progress.
It may not always be possible to reach consensus on the implementation of an idea, so dont feel obliged to achieve
this before you begin. However, always ensure that you keep the outside world informed of your work, and publish it
in a way that allows outsiders to test, discuss and contribute to your efforts.
Contributors on every project come and go. When you leave or disengage from the project, in whole or in part, you
should do so with pride about what you have achieved and by acting responsibly towards others who come after you
to continue the project.
As a user, your feedback is important, as is its form. Poorly thought out comments can cause pain and the demotivation
of other community members, but considerate discussion of problems can bring positive results. An encouraging word
works wonders.
Be pragmatic
ownCloud is a pragmatic community. We value tangible results over having the last word in a discussion. We defend
our core values like freedom and respectful collaboration, but we dont let arguments about minor issues get in the
way of achieving more important results. We are open to suggestions and welcome solutions regardless of their origin.
When in doubt support a solution which helps getting things done over one which has theoretical merits, but isnt
being worked on. Use the tools and methods which help getting the job done. Let decisions be taken by those who do
the work.
Support others in the community
Our community is made strong by mutual respect, collaboration and pragmatic, responsible behavior. Sometimes there
are situations where this has to be defended and other community members need help.
If you witness others being attacked, think first about how you can offer them personal support. If you feel that
the situation is beyond your ability to help individually, go privately to the victim and ask if some form of official
intervention is needed. Similarly you should support anyone who appears to be in danger of burning out, either
through work-related stress or personal problems.
When problems do arise, consider respectfully reminding those involved of our shared Code of Conduct as a first
action. Leaders are defined by their actions, and can help set a good example by working to resolve issues in the spirit
of this Code of Conduct before they escalate.
Get support from others in the community
Disagreements, both political and technical, happen all the time. Our community is no exception to the rule. The goal
is not to avoid disagreements or differing views but to resolve them constructively. You should turn to the community
to seek advice and to resolve disagreements and where possible consult the team most directly involved.
Think deeply before turning a disagreement into a public dispute. If necessary request mediation, trying to resolve
differences in a less highly-emotional medium. If you do feel that you or your work is being attacked, take your time
to breathe through before writing heated replies. Consider a 24 hour moratorium if emotional language is being used
a cooling off period is sometimes all that is needed. If you really want to go a different way, then we encourage you
to publish your ideas and your work, so that it can be tried and tested.
This document is licensed under the Creative Commons Attribution Share Alike 3.0 License.
The authors of this document would like to thank the ownCloud community and those who have worked to create such
a dynamic environment to share in and who offered their thoughts and wisdom in the authoring of this document. We
would also like to thank other vibrant communities that have helped shape this document with their own examples,
especially KDE.
To get started the basic git repositories need to cloned into the web servers directory. Depending on the distribution
this will either be
/var/www
/var/www/html
/srv/http
Then identify the user and group the web server is running as and the Apache user and group for the chown command
will either be
http
www-data
apache
wwwrun
Check out the code
The following commands are using /var/www as the web servers directory and www-data as user name and group.
Install the development tool
After the development tool installation make the directory writable:
sudo chmod o+rw /var/www
Finally restart the web server (this might vary depending on your distribution):
sudo systemctl restart httpd.service
or:
After the clone Open http://localhost/core (or the corresponding URL) in your web browser to set up your instance.
Enabling debug mode
Note: Do not enable this for production! This can create security problems and is only meant for debugging and
development!
To disable JavaScript and CSS caching debugging has to be enabled in core/config/config.php by adding
this to the end of the file:
DEFINE('DEBUG', true);
If you have more than one repository cloned, it can be time consuming to do the same the action to all repositories one
by one. To solve this, you can use the following command template:
find . -maxdepth <DEPTH> -type d -name .git -exec sh -c 'cd "{}"/../ && pwd && <GIT COMMAND>' \;
then, e.g. to pull all changes in all repositories, you only need this:
find . -maxdepth 3 -type d -name .git -exec sh -c 'cd "{}"/../ && pwd && git pull --rebase' \;
find . -maxdepth 3 -type d -name .git -exec sh -c 'cd "{}"/../ && pwd && git remote prune origin' \;
It is even easier if you create alias from these commands in case you want to avoid retyping those each time you need
them.
SQL Injection
SQL Injection occurs when SQL query strings are concatenated with variables.
To prevent this, always use prepared queries:
<?php
$sql = 'SELECT * FROM `users` WHERE `id` = ?';
$query = \OCP\DB::prepare($sql);
$params = array(1);
$result = $query->execute($params);
If the App Framework is used, write SQL queries like this in the a class that extends the Mapper:
<?php
// inside a child mapper class
$sql = 'SELECT * FROM `users` WHERE `id` = ?';
$params = array(1);
$result = $this->execute($sql, $params);
to overtake the user account. The same problem occurs when outputting content from the database or any other
location that is writable by users.
Another attack vector that is often overlooked is XSS in href attributes. HTML allows to execute javascript in href
attributes like this:
<a href="javascript:alert('xss')">
To prevent XSS in your app, never use echo, print() or <%= - use p() instead which will sanitize the input. Also
validate URLs to start with the expected protocol (starts with http for instance)!
Note: Should you ever require to print something unescaped, double check if it is really needed. If there is no other
way (e.g. when including of subtemplates) use print_unescaped with care.
JavaScript
Avoid manipulating the HTML directly via JavaScript, this often leads to XSS since people often forget to sanitize
variables:
var html = '<li>' + username + '</li>"';
If you really want to use JavaScript for something like this use escapeHTML to sanitize the variables:
var html = '<li>' + escapeHTML(username) + '</li>';
An even better way to make your app safer is to use the jQuery built-in function $.text() instead of $.html().
DONT
messageTd.html(username);
DO
messageTd.text(username);
It may also be wise to choose a proper JavaScript framework like AngularJS which automatically handles the
JavaScript escaping for you.
Clickjacking
Clickjacking tricks the user to click into an invisible iframe to perform an arbitrary action (e.g. delete an user account)
To prevent such attacks ownCloud sends the X-Frame-Options header to all template responses. Dont remove this
header if you dont really need it!
This is already built into ownCloud if OC_Template.
Code executions / File inclusions
Code Execution means that an attacker is able to include an arbitrary PHP file. This PHP file runs with all the privileges
granted to the normal application and can do an enormous amount of damage.
Code executions and file inclusions can be easily prevented by never allowing user-input to run through the following
functions:
include()
require()
require_once()
eval()
1.1. General Contributor Guidelines
fopen()
Note: Also never allow the user to upload files into a folder which is reachable from the URL!
DONT
<?php
require("/includes/" . $_GET['file']);
Note: If you have to pass user input to a potential dangerous, double check to be sure that there is no other way. If it
is not possible otherwise sanitize every user parameter and ask people to audit your sanitize function.
Directory Traversal
Very often developers forget about sanitizing the file path (removing all and /), this allows an attacker to traversal
through directories on the server which opens several potential attack vendors including privilege escalations, code
executions or file disclosures.
DONT
<?php
$username = OC_User::getUser();
fopen("/data/" . $username . "/" . $_GET['file'] . ".txt");
DO
<?php
$username = OC_User::getUser();
$file = str_replace(array('/', '\\'), '', $_GET['file']);
fopen("/data/" . $username . "/" . $file . ".txt");
Note: PHP also interprets the backslash () in paths, dont forget to replace it too!
Shell Injection
Shell Injection occurs if PHP code executes shell commands (e.g. running a latex compiler). Before doing this, check
if there is a PHP library that already provides the needed functionality. If you really need to execute a command be
aware that you have to escape every user parameter passed to one of these functions:
exec()
shell_exec()
passthru()
proc_open()
system()
popen()
Note: Please require/request additional programmers to audit your escape function.
10
Without escaping the user input this will allow an attacker to execute arbitrary shell commands on your server.
PHP offers the following functions to escape user input:
escapeshellarg(): Escape a string to be used as a shell argument
escapeshellcmd(): Escape shell metacharacters
DONT
<?php
system('ls '.$_GET['dir']);
DO
<?php
system('ls '.escapeshellarg($_GET['dir']));
If you are using the App Framework, every controller method is automatically checked for CSRF unless you explicitly
exclude it by setting the @NoCSRFRequired annotation before the controller method, see Controllers
1.1. General Contributor Guidelines
11
Unvalidated redirects
This is more of an annoyance than a critical security vulnerability since it may be used for social engineering or
phishing.
Always validate the URL before redirecting if the requested URL is on the same domain or an allowed ressource.
DONT
<?php
header('Location:'. $_GET['redirectURL']);
DO
<?php
header('Location: http://www.example.com'. $_GET['redirectURL']);
Getting help
If you need help to ensure that a function is secure please ask on our mailing list or on our IRC channel #owncloud-dev
on irc.freenode.net.
12
Labels
We assign labels to issues and pull requests to make it easy to find them and to signal what needs to be done. Some
of these are assigned by the developers, others by QA, bug triagers, project lead or maintainers and so on. It is not
desired that users/reporters of bugs assign labels themselves, unless they are developers/contributors to ownCloud.
The most important labels and their meaning:
#bug - this issue is a bug
#enhancement - this issue is a feature request/idea for improvement of ownCloud
#design - this needs help from the design team or is a design-related issue/pull request
#sharing - this issue or PR is related to sharing
#technical debt - this issue or PR is about technical debt
#sev1-critical #sev2-high #sev3-medium #sev4-low signify how important the bug is.
#p1-urgent #p2-high #p3-medium #p4-low signify the priority of the bug.
#Junior Job - these are issues which are relatively easy to solve and ideal for people who want to learn how to
code in ownCloud
Tags showing the state of the issue or PR, numbered 1-6:
#1 - Backlog - (please dont use, we prefer using a backlog milestone)
#2 - Triaging - (please dont use, we prefer using the triage label)
#3 - To develop - ready to start development on this
#4 - Developing - development in progress
#5 - To Review - ready for review
#6 - Reviewing - review in progress
#7 - To Release - reviewed PR that awaits unfreeze of a branch to get merged
App tags: #app:files #app:user_ldap #app:files_versions and so on. These tags indicate the app that is impacted
by the issue or which the PR is related to
settings tags: #settings:personal #settings:apps #settings:admin and so on. These tags indicate the settings area
that is impacted by the issue or which the PR is related to
db tags: #db:mysql #db:sqlite #db:postgresql and so on. These tags indicate the database that is impacted by the
issue or which the PR is related to
browser tags: #browser:ie #browser:safari and so on. These tags indicate the browser that is impacted by the
issue or which the PR is related to
#triage - this issue has to be triaged
#needs info - this issue needs further information from the reporter, see triaging old tag is #clarification request,
please dont use that one anymore.
#discussion - this issue needs to be discussed
#security - this is a security related issue
#windows server - this is related to windows server
#research - this item requires some research before it can continue
#packaging - this is related to packaging
13
14
<?php
should not be used at the end of the file due to the possible issue of sending white spaces.
Comments
All API methods need to be marked with PHPDoc markup. An example would be:
<?php
/**
* Description what method does
* @param Controller $controller the controller that will be transformed
* @param API $api an instance of the API class
* @throws APIException if the api is broken
* @since 4.5
* @return string a name of a user
*/
public function myMethod(Controller $controller, API $api) {
// ...
}
Use Pascal case for Objects, Camel case for functions and variables. If you set a default function/method parameter,
do not use spaces. Do not prepend private class members with underscores.
class MyClass {
}
function myFunction($default=null) {
}
$myVariable = 'blue';
$someArray = array(
'foo' => 'bar',
'spam' => 'ham',
);
?>
Operators
15
Heres why:
<?php
var_dump(0 == "a"); // 0 == 0 -> true
var_dump("1" == "01"); // 1 == 1 -> true
var_dump("10" == "1e1"); // 10 == 10 -> true
var_dump(100 == "1e2"); // 100 == 100 -> true
?>
Control Structures
16
Unit tests
Unit tests must always extend the \Test\TestCase class, which takes care of cleaning up the installation after the
test.
If a test is run with multiple different values, a data provider must be used. The name of the data provider method
must not start with test and must end with Data.
<?php
namespace Test;
class Dummy extends \Test\TestCase {
public function dummyData() {
return array(
array(1, true),
array(2, false),
);
}
/**
* @dataProvider dummyData
*/
public function testDummy($input, $expected) {
$this->assertEquals($expected, \Dummy::method($input));
}
}
JavaScript
In general take a look at JSLint without the whitespace rules.
Use a js/main.js or js/app.js where your program is started
Complete every statement with a ;
Use var to limit variable to local scope
To keep your code local, wrap everything in a self executing function. To access global objects or export things
to the global namespace, pass all global objects to the self executing function.
Use JavaScript strict mode
Use a global namespace object where you bind publicly used functions and objects to
DO:
// set up namespace for sharing across multiple files
var MyApp = MyApp || {};
(function(window, $, exports, undefined) {
'use strict';
// if this function or object should be global, attach it to the namespace
exports.myGlobalFunction = function(params) {
return params;
};
})(window, jQuery, MyApp);
17
DONT (Seriously):
// This does not only make everything global but you're programming
// JavaScript like C functions with namespaces
MyApp = {
myFunction:function(params) {
return params;
},
...
};
Try to use OOP in your JavaScript to make your code reusable and flexible.
This is how youd do inheritance in JavaScript:
// create parent object and bind methods to it
var ParentObject = function(name) {
this.name = name;
};
ParentObject.prototype.sayHello = function() {
console.log(this.name);
}
Use Pascal case for Objects, Camel case for functions and variables.
18
Operators
// false
// true
// true
false == 'false'
false == '0'
// false
// true
false == undefined
false == null
null == undefined
// false
// false
// true
// true
Control Structures
19
) {
// your code
}
// for loop
for (var i = 0; i < 4; i++) {
// your code
}
// switch
switch (value) {
case 'hi':
// yourcode
break;
default:
console.warn('Entered undefined default block in switch');
break;
}
CSS
Take a look at the Writing Tactical CSS & HTML video on YouTube.
Dont bind your CSS too much to your HTML structure and try to avoid IDs. Also try to make your CSS reusable by
grouping common attributes into classes.
DO:
.list {
list-style-type: none;
}
.list > .list_item {
display: inline-block;
}
.important_list_item {
color: red;
}
DONT:
#content .myHeader ul {
list-style-type: none;
}
#content .myHeader ul li.list_item {
color: red;
display: inline-block;
}
TBD
20
21
If you increase the long_query_time to 100 and add log-queries-not-using-indexes, all the queries that are not using
an index are logged. Every query should always use an index. So ideally there should be no output:
log-queries-not-using-indexes
log_slow_queries = 1
log_slow_queries = /var/log/mysql/mysql-slow.log
long_query_time=100
Measuring performance
If you do bigger changes in the architecture or the database structure you should always double check the positive or
negative performance impact. There are a few nice small scripts that can be used for this.
The recommendation is to automatically do 10000 PROPFINDs or file uploads, measure the time and compare the
time before and after the change.
Getting help
If you need help with performance or other issues please ask on our mailing list or on our IRC channel #owncloud-dev
on irc.freenode.net.
1.1.6 Debugging
Debug mode
When debug mode is enabled ownCloud, a variety of debugging features are enabled - see debugging documentation.
Add the following to the very end of /config/config.php to enable it:
define( "DEBUG", 1);
Identifying errors
ownCloud uses custom error PHP handling that prevents errors being printed to web server log files or command line
output. Instead, errors are generally stored in ownClouds own log file, located at: /data/owncloud.log
Debugging variables
You should use exceptions if you need to debug variable values manually, and not alternatives like trigger_error()
(which may not be logged).
e.g.:
<?php throw new \Exception( "\$user = $user" ); // should be logged in ownCloud ?>
not:
<?php trigger_error( "\$user = $user" ); // may not be logged anywhere ?>
To disable custom error handling in ownCloud (and have PHP and your web server handle errors instead), see Debug
mode.
22
Debugging Javascript
By default all Javascript files in ownCloud are minified (compressed) into a single file without whitespace. To prevent
this, see Debug mode.
Debugging HTML and templates
By default ownCloud caches HTML generated by templates. This may prevent changes to app templates, for example,
from being applied on page refresh. To disable caching, see Debug mode.
Using alternative app directories
It may be useful to have multiple app directories for testing purposes, so you can conveniently switch between different
versions of applications. See the configuration file documentation for details.
1.1.7 Backporting
General
We backport important fixes and improvements from the current master release to get them to our users faster.
Process
We mostly consider bug fixes for back porting. Occasionally, important changes to the API can be backported to make
it easier for developers to keep their apps working between major releases. If you think a pull request (PR) is relevant
for the stable release, go through these steps:
1. Make sure the PR is merged to master
2. Ask Frank (@karlitschek) and Thomas (@deepdiver1975) if the code should be backported and add the label
backport-request to the PR
3. If Frank or Thomas say yes then create a new branch based on the respective stable branch (stable7 for the 7.0.x
series), cherry-pick the needed commits to that branch and create a PR on GitHub.
4. Specify the corresponding milestone for that series (7.0.x-next-maintenance for the 7.0.x series) to this PR and
reference the original PR in there. This enables the QA team to find the backported items for testing and having
the original PR with detailed description linked.
Note: Before each patch release there is a freeze to be able to test everything as a whole without pulling in new
changes. This freeze is announced on the owncloud-devel mailinglist. While this freeze is active a backport isnt
allowed and has to wait for the next patch release.
The QA team will try to reproduce all the issues with the X.Y.Z-next-maintenance milestone on the relevant release
and verify it is fixed by the patch release (and doesnt cause new problems). Once the patch release is out, the post-fix
-next-maintenance is removed and a new -next-maintenance milestone is created for that series.
1.2 Changelog
The following changes went into ownCloud 8.1:
1.2. Changelog
23
1.2.2 Features
There is a new OCSResponse and OCSController which allows you to easily migrate OCS code to the App
Framework. This was added purely for compatibility reasons and the preferred way of doing APIs is using a
RESTful API
You can now stream files in PHP by using the built in StreamResponse.
For more advanced usecases you can now implement the CallbackResponse interface which allows your response to do its own response rendering
1.2.3 Deprecations
This is a deprecation roadmap which lists all current deprecation targets and will be updated from release to release.
This lists the version when a specific method or class will be removed.
Note: Deprecations on interfaces also affect the implementing classes!
10.0
OCP\IDb: This interface and the implementing classes will be removed in favor of OCP\IDbConnection.
Various layers in between have also been removed to be consistent with the PDO classes. This leads to the
following changes:
Replace all calls on the db using getInsertId with lastInsertId
Replace all calls on the db using prepareQuery with prepare
The __construct method of OCP\AppFramework\Db\Mapper no longer requires an instance of OCP\IDb
but an instance of OCP\IDbConnection
The execute method on OCP\AppFramework\Db\Mapper no longer returns an instance of
OC_DB_StatementWrapper but an instance of PDOStatement
9.0
The following methods have been moved into the OCP\Template::<method> class instead of being namespaced directly:
OCP\image_path
OCP\mimetype_icon
OCP\preview_icon
OCP\publicPreview_icon
OCP\human_file_size
OCP\relative_modified_date
OCP\html_select_options
24
1.3 Tutorial
This tutorial will outline how to create a very simple notes app. The finished app is available on GitHub.
1.3.1 Setup
After the development tool has been installed the development environment needs to be set up. This can be done by
either downloading the zip from the website or cloning it directly from GitHub:
ocdev setup core --dir owncloud
--branch $BRANCH
Note: $BRANCH is the desired ownCloud branch (e.g. stable7 for ownCloud 7, stable8 for ownCloud 8, etc)
First you want to enable debug mode to get proper error messages. To do that add DEFINE(DEBUG, true); at the
end of the owncloud/config/config.php file:
echo "\nDEFINE('DEBUG', true);" >> owncloud/config/config.php
1.3. Tutorial
25
This creates a new folder called ownnotes. Now access and set up ownCloud through the webinterface at
http://localhost:8080 and enable the OwnNotes application on the apps page.
The first basic app is now available at http://localhost:8080/index.php/apps/ownnotes/
On the server side we need to register a callback that is executed once the request comes in. The callback itself will
be a method on a controller and the controller will be connected to the URL with a route. The controller and route for
the page are already set up in ownnotes/appinfo/routes.php:
<?php
return ['routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET']
]];
This route calls the controller OCA\OwnNotes\PageController->index() method which is defined in ownnotes/controller/pagecontroller.php. The controller returns a template, in this case ownnotes/templates/main.php:
Note: @NoAdminRequired and @NoCSRFRequired in the comments above the method turn off security checks, see
Controllers
26
<?php
namespace OCA\OwnNotes\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Controller;
class PageController extends Controller {
public function __construct($AppName, IRequest $request){
parent::__construct($AppName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index() {
return new TemplateResponse('ownnotes', 'main');
}
}
Since the route which returns the intial HTML has been taken care of, the controller which handles the AJAX requests
for the notes needs to be set up. Create the following file: ownnotes/controller/notecontroller.php with the following
content:
<?php
namespace OCA\OwnNotes\Controller;
use OCP\IRequest;
use OCP\AppFramework\Controller;
class NoteController extends Controller {
public function __construct($AppName, IRequest $request){
parent::__construct($AppName, $request);
}
/**
* @NoAdminRequired
*/
public function index() {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
*/
public function show($id) {
// empty for now
}
/**
* @NoAdminRequired
1.3. Tutorial
27
*
* @param string $title
* @param string $content
*/
public function create($title, $content) {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
public function update($id, $title, $content) {
// empty for now
}
/**
* @NoAdminRequired
*
* @param int $id
*/
public function destroy($id) {
// empty for now
}
}
Note: The parameters are extracted from the request body and the url using the controller methods variable names.
Since PHP does not support type hints for primitive types such as ints and booleans, we need to add them as annotations
in the comments. In order to type cast a parameter to an int, add @param int $parameterName
Now the controller methods need to be connected to the corresponding URLs in the ownnotes/appinfo/routes.php
file:
<?php
return [
'routes' =>
['name'
['name'
['name'
['name'
['name'
['name'
]
];
[
=>
=>
=>
=>
=>
=>
Since those 5 routes are so common, they can be abbreviated by adding a resource instead:
<?php
return [
'resources' => [
'note' => ['url' => '/notes']
],
28
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET']
]
];
1.3.3 Database
Now that the routes are set up and connected the notes should be saved in the database. To do that first create a
database schema by creating ownnotes/appinfo/database.xml:
<database>
<name>*dbname*</name>
<create>true</create>
<overwrite>false</overwrite>
<charset>utf8</charset>
<table>
<name>*dbprefix*ownnotes_notes</name>
<declaration>
<field>
<name>id</name>
<type>integer</type>
<notnull>true</notnull>
<autoincrement>true</autoincrement>
<unsigned>true</unsigned>
<primary>true</primary>
<length>8</length>
</field>
<field>
<name>title</name>
<type>text</type>
<length>200</length>
<default></default>
<notnull>true</notnull>
</field>
<field>
<name>user_id</name>
<type>text</type>
<length>200</length>
<default></default>
<notnull>true</notnull>
</field>
<field>
<name>content</name>
<type>clob</type>
<default></default>
<notnull>true</notnull>
</field>
</declaration>
</table>
</database>
To create the tables in the database, the version tag in ownnotes/appinfo/info.xml needs to be increased:
<?xml version="1.0"?>
<info>
1.3. Tutorial
29
<id>ownnotes</id>
<name>Own Notes</name>
<description>My first ownCloud app</description>
<licence>AGPL</licence>
<author>Your Name</author>
<version>0.0.2</version>
<namespace>OwnNotes</namespace>
<category>other</category>
<dependencies>
<owncloud min-version="8" />
</dependencies>
</info>
30
Note: The first parent constructor parameter is the database layer, the second one database table and the third is the
entity on which the result should be mapped onto. Insert, delete and update methods are already implemented.
OCP\IRequest;
OCP\AppFramework\Http;
OCP\AppFramework\Http\DataResponse;
OCP\AppFramework\Controller;
use OCA\OwnNotes\Db\Note;
use OCA\OwnNotes\Db\NoteMapper;
class NoteController extends Controller {
private $mapper;
private $userId;
public function __construct($AppName, IRequest $request, NoteMapper $mapper, $UserId){
parent::__construct($AppName, $request);
$this->mapper = $mapper;
$this->userId = $UserId;
}
/**
* @NoAdminRequired
*/
public function index() {
1.3. Tutorial
31
32
}
$this->mapper->delete($note);
return new DataResponse($note);
}
}
Note:
The actual exceptions are OCP\AppFramework\Db\DoesNotExistException and
OCP\AppFramework\Db\MultipleObjectsReturnedException but in this example we will treat them as the
same. DataResponse is a more generic response than JSONResponse and also works with JSON.
This is all that is needed on the server side. Now lets progress to the client side.
1.3.5 Making things reusable and decoupling controllers from the database
Lets say our app is now on the app store and and we get a request that we should save the files in the filesystem which
requires access to the filesystem.
The filesystem API is quite different from the database API and throws different exceptions, which means we need to
rewrite everything in the NoteController class to use it. This is bad because a controllers only responsibility should
be to deal with incoming Http requests and return Http responses. If we need to change the controller because the data
storage was changed the code is probably too tightly coupled and we need to add another layer in between. This layer
is called Service.
Lets take the logic that was inside the controller and put it into a separate class inside ownnotes/service/noteservice.php:
<?php
namespace OCA\OwnNotes\Service;
use Exception;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\OwnNotes\Db\Note;
use OCA\OwnNotes\Db\NoteMapper;
class NoteService {
private $mapper;
public function __construct(NoteMapper $mapper){
$this->mapper = $mapper;
}
public function findAll($userId) {
return $this->mapper->findAll($userId);
}
private function handleException ($e) {
if ($e instanceof DoesNotExistException ||
$e instanceof MultipleObjectsReturnedException) {
throw new NotFoundException($e->getMessage());
1.3. Tutorial
33
} else {
throw $e;
}
}
public function find($id, $userId) {
try {
return $this->mapper->find($id, $userId);
// in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions
// into service related exceptions so controllers and service users
// have to deal with only one type of exception
} catch(Exception $e) {
$this->handleException($e);
}
}
public function create($title, $content, $userId) {
$note = new Note();
$note->setTitle($title);
$note->setContent($content);
$note->setUserId($userId);
return $this->mapper->insert($note);
}
public function update($id, $title, $content, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$note->setTitle($title);
$note->setContent($content);
return $this->mapper->update($note);
} catch(Exception $e) {
$this->handleException($e);
}
}
public function delete($id, $userId) {
try {
$note = $this->mapper->find($id, $userId);
$this->mapper->delete($note);
return $note;
} catch(Exception $e) {
$this->handleException($e);
}
}
}
34
and ownnotes/service/notfoundexception.php:
<?php
namespace OCA\OwnNotes\Service;
class NotFoundException extends ServiceException {}
Remember how we had all those ugly try catches that where checking for DoesNotExistException and simply returned
a 404 response? Lets also put this into a reusable class. In our case we chose a trait so we can inherit methods without
having to add it to our inheritance hirarchie. This will be important later on when youve got controllers that inherit
from the ApiController class instead.
The trait is created in ownnotes/controller/errors.php:
<?php
namespace OCA\OwnNotes\Controller;
use Closure;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCA\OwnNotes\Service\NotFoundException;
trait Errors {
protected function handleNotFound (Closure $callback) {
try {
return new DataResponse($callback());
} catch(NotFoundException $e) {
$message = ['message' => $e->getMessage()];
return new DataResponse($message, Http::STATUS_NOT_FOUND);
}
}
}
Now we can wire up the trait and the service inside the NoteController:
<?php
namespace OCA\OwnNotes\Controller;
use OCP\IRequest;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
use OCA\OwnNotes\Service\NoteService;
class NoteController extends Controller {
private $service;
private $userId;
use Errors;
1.3. Tutorial
35
36
Great! Now the only reason that the controller needs to be changed is when request/response related things change.
$controller;
$service;
$userId = 'john';
$request;
1.3. Tutorial
37
$this->equalTo($this->userId))
->will($this->returnValue($note));
$result = $this->controller->update(3, 'title', 'content');
$this->assertEquals($note, $result->getData());
}
We can and should also create a test for the NoteService class:
<?php
namespace OCA\OwnNotes\Service;
use PHPUnit_Framework_TestCase;
use OCP\AppFramework\Db\DoesNotExistException;
use OCA\OwnNotes\Db\Note;
class NoteServiceTest extends PHPUnit_Framework_TestCase {
private $service;
private $mapper;
private $userId = 'john';
public function setUp() {
$this->mapper = $this->getMockBuilder('OCA\OwnNotes\Db\NoteMapper')
->disableOriginalConstructor()
->getMock();
$this->service = new NoteService($this->mapper);
}
public function testUpdate() {
// the existing note
$note = Note::fromRow([
'id' => 3,
'title' => 'yo',
'content' => 'nope'
]);
$this->mapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->returnValue($note));
38
/**
* @expectedException OCA\OwnNotes\Service\NotFoundException
*/
public function testUpdateNotFound() {
// test the correct status code if no note is found
$this->mapper->expects($this->once())
->method('find')
->with($this->equalTo(3))
->will($this->throwException(new DoesNotExistException('')));
$this->service->update(3, 'title', 'content', $this->userId);
}
}
If PHPUnit is installed we can run the tests inside ownnotes/ with the following command:
phpunit
Note: You need to adjust the ownnotes/tests/unit/controller/PageControllerTest file to get the tests passing: remove
the testEcho method since that method is no longer present in your PageController and do not test the user id
parameters since they are not passed anymore
Integration Tests
Integration tests are slow and need a fully working instance but make sure that our classes work well together. Instead
of mocking out all classes and parameters we can decide wether to use full instances or replace certain classes. Because
they are slow we dont want as many integration tests as unit tests.
In our case we want to create an integration test for the udpate method without mocking out the NoteMapper class so
we actually write to the existing database.
To do that create a new file called ownnotes/tests/integration/NoteIntegrationTest.php with the following content:
<?php
namespace OCA\OwnNotes\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\App;
1.3. Tutorial
39
use Test\TestCase;
use OCA\OwnNotes\Db\Note;
class NoteIntregrationTest extends TestCase {
private $controller;
private $mapper;
private $userId = 'john';
public function setUp() {
parent::setUp();
$app = new App('ownnotes');
$container = $app->getContainer();
// only replace the user id
$container->registerService('UserId', function($c) {
return $this->userId;
});
$this->controller = $container->query(
'OCA\OwnNotes\Controller\NoteController'
);
$this->mapper = $container->query(
'OCA\OwnNotes\Db\NoteMapper'
);
}
public function testUpdate() {
// create a new note that should be updated
$note = new Note();
$note->setTitle('old_title');
$note->setContent('old_content');
$note->setUserId($this->userId);
$id = $this->mapper->insert($note)->getId();
// fromRow does not set the fields as updated
$updatedNote = Note::fromRow([
'id' => $id,
'user_id' => $this->userId
]);
$updatedNote->setContent('content');
$updatedNote->setTitle('title');
$result = $this->controller->update($id, 'title', 'content');
$this->assertEquals($updatedNote, $result->getData());
// clean up
$this->mapper->delete($result->getData());
}
}
To run the integration tests change into the ownnotes directory and run:
40
phpunit -c phpunit.integration.xml
1.3. Tutorial
41
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param string $title
* @param string $content
*/
public function create($title, $content) {
return $this->service->create($title, $content, $this->userId);
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param int $id
* @param string $title
* @param string $content
*/
public function update($id, $title, $content) {
return $this->handleNotFound(function () use ($id, $title, $content) {
return $this->service->update($id, $title, $content, $this->userId);
});
}
/**
* @CORS
* @NoCSRFRequired
* @NoAdminRequired
*
* @param int $id
*/
public function destroy($id) {
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
});
}
}
All that is left is to connect the controller to a route and enable the built in preflighted CORS method which is defined
in the ApiController base class:
<?php
return [
'resources' => [
'note' => ['url' => '/notes'],
'note_api' => ['url' => '/api/0.1/notes']
],
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
['name' => 'note_api#preflighted_cors', 'url' => '/api/0.1/{path}',
'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']]
42
]
];
Since the NoteApiController is basically identical to the NoteController, the unit test for it simply inherits its tests
from the NoteControllerTest. Create the file ownnotes/tests/unit/controller/NoteApiControllerTest.php:
<?php
namespace OCA\OwnNotes\Controller;
require_once __DIR__ . '/NoteControllerTest.php';
class NoteApiControllerTest extends NoteControllerTest {
public function setUp() {
parent::setUp();
$this->controller = new NoteApiController(
'ownnotes', $this->request, $this->service, $this->userId
);
}
}
1.3. Tutorial
43
1.3.11 Wiring it up
When the page is loaded we want all the existing notes to load. Furthermore we want to display the current note when
you click on it in the navigation, a note should be deleted when we click the deleted button and clicking on New note
should create a new note. To do that open ownnotes/js/script.js and replace the example code with the following:
(function (OC, window, $, undefined) {
'use strict';
$(document).ready(function () {
var translations = {
newNote: $('#new-note-string').text()
};
44
1.3. Tutorial
45
url: this._baseUrl,
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(note)
}).done(function (note) {
self._notes.push(note);
self._activeNote = note;
self.load(note.id);
deferred.resolve();
}).fail(function () {
deferred.reject();
});
return deferred.promise();
},
getAll: function () {
return this._notes;
},
loadAll: function () {
var deferred = $.Deferred();
var self = this;
$.get(this._baseUrl).done(function (notes) {
self._activeNote = undefined;
self._notes = notes;
deferred.resolve();
}).fail(function () {
deferred.reject();
});
return deferred.promise();
},
updateActive: function (title, content) {
var note = this.getActive();
note.title = title;
note.content = content;
return $.ajax({
url: this._baseUrl + '/' + note.id,
method: 'PUT',
contentType: 'application/json',
data: JSON.stringify(note)
});
}
};
// this will be the view that is used to update the html
var View = function (notes) {
this._notes = notes;
};
View.prototype = {
renderContent: function () {
var source = $('#content-tpl').html();
var template = Handlebars.compile(source);
var html = template({note: this._notes.getActive()});
$('#editor').html(html);
// handle saves
var textarea = $('#app-content textarea');
46
1.3. Tutorial
47
$('#editor textarea').focus();
});
},
render: function () {
this.renderNavigation();
this.renderContent();
}
};
var notes = new Notes(OC.generateUrl('/apps/ownnotes/notes'));
var view = new View(notes);
notes.loadAll().done(function () {
view.render();
}).fail(function () {
alert('Could not load notes');
});
});
})(OC, window, jQuery);
48
}
#editor button {
height: 44px;
}
Congratulations! Youve written your first ownCloud app. You can now either try to further improve the tutorial notes
app or start writing your own app.
Then run:
ocdev startapp MyApp --email [email protected] --author "Your Name" --description "My first app" --ow
This will create all the needed files in the current directory. For more information on how to customize the generated
app, see the Projects GitHub page or run:
ocdev startapp -h
49
<?php
\OC::$server->getNavigationManager()->add(function () {
$urlGenerator = \OC::$server->getURLGenerator();
return [
// the string under which your app will be referenced in owncloud
'id' => 'myapp',
// sorting weight for the navigation. The higher the number, the higher
// will it be listed in the navigation
'order' => 10,
// the route that will be shown on startup
'href' => $urlGenerator->linkToRoute('myapp.page.index'),
// the icon that will be shown in the navigation
// this file needs to exist in img/
'icon' => $urlGenerator->imagePath('myapp', 'app.svg'),
// the title of your application. This will be used in the
// navigation or on the settings page of your app
'name' => \OC::$server->getL10N('myapp')->t('My App'),
];
});
// execute OCA\MyApp\BackgroundJob\Task::run when cron is called
\OC::$server->getJobList()->add('OCA\MyApp\BackgroundJob\Task');
// execute OCA\MyApp\Hooks\User::deleteUser before a user is being deleted
\OCP\Util::connectHook('OC_User', 'pre_deleteUser', 'OCA\MyApp\Hooks\User', 'deleteUser');
Although it is also possible to include JavaScript or CSS for other apps by placing the addScript or addStyle functions
inside this file, it is strongly discouraged, because the file is loaded on each request (also such requests that do not
return HTML, but e.g. json or webdav).
<?php
\OCP\Util::addScript('myapp', 'script'); // include js/script.js for every app
\OCP\Util::addStyle('myapp', 'style'); // include css/style.css for every app
50
<types>
<type>filesystem</type>
</types>
<documentation>
<user>http://doc.owncloud.org</user>
<admin>http://doc.owncloud.org</admin>
</documentation>
<category>other</category>
<website>http://www.owncloud.org</website>
<bugs>http://github.com/owncloud/theapp/issues</bugs>
<repository type="git">http://github.com/owncloud/theapp.git</repository>
<ocsid>1234</ocsid>
<dependencies>
<php min-version="5.4" max-version="5.5"/>
<database>sqlite</database>
<database>mysql</database>
<command os="linux">grep</command>
<command os="windows">notepad.exe</command>
<lib min-version="1.2">xml</lib>
<lib max-version="2.0">intl</lib>
<lib>curl</lib>
<os>Linux</os>
<owncloud min-version="6.0.4" max-version="8"/>
</dependencies>
<!-- deprecated, just for reference -->
<public>
<file id="caldav">appinfo/caldav.php</file>
</public>
<remote>
<file id="caldav">appinfo/caldav.php</file>
</remote>
<standalone />
<default_enable />
<shipped>true</shipped>
<!-- end deprecated -->
</info>
1.6.1 id
Required: This field contains the internal app name, and has to be the same as the folder name of the app. This id
needs to be unique in ownCloud, meaning no other app should have this id.
51
1.6.2 name
Required: This is the human-readable name/title of the app that will be displayed in the app overview page.
1.6.3 description
Required: This contains the description of the app which will be shown in the apps overview page.
1.6.4 version
Contains the version of your app. Please also provide the same version in the appinfo/version.
1.6.5 licence
Required: The licence of the app. This licence must be compatible with the AGPL and must not be proprietary, for
instance:
AGPL 3 (recommended)
MIT
If a proprietary/non AGPL compatible licence should be used, the ownCloud Enterprise Edition must be used.
1.6.6 author
Required: The name of the app author or authors.
1.6.7 requiremin
Required if not added in the <dependencies> tag. The minimal version of ownCloud.
1.6.8 namespace
Required if routes.php returns an array. If your app is namespaced like \OCA\MyApp\Controller\PageController
the required namespace value is MyApp. If not given it tries to default to the first letter upper cased app id, e.g. myapp
would be tried under Myapp
1.6.9 types
ownCloud allows to specify four kind of types. Currently supported types:
prelogin: apps which needs to load on the login page
filesystem: apps which provides filesystem functionality (e.g. files sharing app)
authentication: apps which provided authentication backends
logging: apps which implement a logging system
52
1.6.10 documentation
link to admin and user documentation
1.6.11 website
link to project web page
1.6.12 repository
Link to the version control repo
1.6.13 bugs
Link to the bug tracker
1.6.14 category
Category on the app store. Can be one of the following:
other
multimedia
pim
productivity
games
tools
1.6.15 ocsid
The apps id on the app store, e.g.: https://apps.owncloud.com/content/show.php/QOwnNotes?content=168497 would
have the ocsid 168497. If given helps users to install and update the same app from the app store
Dependencies
All tags within the dependencies tag define a set of requirements which have to be fulfilled in order to operate properly.
As soon as one of these requirements is not met the app cannot be installed.
1.6.16 php
Defines the minimum and the maximum version of php which is required to run this app.
1.6.17 database
Each supported database has to be listed in here. Valid values are sqlite, mysql, pgsql, oci and mssql. In the future it
will be possible to specify versions here as well. In case no database is specified it is assumed that all databases are
supported.
1.6. App Metadata
53
1.6.18 command
Defines a command line tool to be available. With the attribute os the required operating system for this tool can be
specified. Valid values for the os attribute are as returned by the php function php_uname.
1.6.19 lib
Defines a required php extension with required minimum and/or maximum version. The names for the libraries have
to match the result as returned by the php function get_loaded_extensions. The explicit version of an extension is read
from phpversion - with some exception as to be read up in the code base
1.6.20 os
Defines the required target operating system the app can run on. Valid values are as returned by the php function
php_uname.
1.6.21 owncloud
Defines minimum and maximum versions of the ownCloud core. In case undefined the values will be taken from the
tag requiremin.
Deprecated
The following sections are just listed for reference and should not be used because
public/remote: Use RESTful API instead because youll have to use External API which is known to be buggy
(works only properly with GET/POST)
standalone/default_enable: They tell core what do on setup, you will not be able to even activate your app if it
has those entries. This should be replaced by a config file inside core.
1.6.22 public
Used to provide a public interface (requires no login) for the app.
cloud/index.php/public. Example with id set to calendar:
/owncloud/index.php/public/calendar
1.6.23 remote
Same as public but requires login. The id is appended to the URL /owncloud/index.php/remote. Example with id set
to calendar:
/owncloud/index.php/remote/calendar
54
1.6.24 standalone
Can be set to true to indicate that this app is a webapp. This can be used to tell GNOME Web for instance to treat this
like a native application.
1.6.25 default_enable
Core apps only: Used to tell ownCloud to enable them after the installation.
1.6.26 shipped
Core apps only: Used to tell ownCloud that the app is in the standard release.
Please note that if this attribute is set to FALSE or not set at all, every time you disable the application, all the files of
the application itself will be REMOVED from the server!
1.7 Classloader
The classloader is provided by ownCloud and loads all your classes automatically. The only thing left to include by
yourself are 3rdparty libraries. Those should be loaded in appinfo/application.php.
The classloader works like this:
Take the full qualifier of a class:
\OCA\MyApp\Controller\PageController
Replace \ with /:
/myapp/controller/pagecontroller
Append .php:
/myapp/controller/pagecontroller.php
Prepend /apps because of the OCA namespace and include the file:
require_once '/apps/myapp/controller/pagecontroller.php';
1.7. Classloader
55
In
other
words:
In
order
for
the
PageController
class
the
class
\OCA\MyApp\Controller\PageController
needs
to
be
/apps/myapp/controller/pagecontroller.php
to
be
stored
autoloaded,
in
the
1.8.2 Router
The router parses the apps routing files (appinfo/routes.php), inspects the requests method and url, queries
the controller from the Container and then passes control to the dispatcher. The dispatcher is responsible for running
the hooks (called Middleware) before and after the controller, executing the controller method and rendering the
output.
56
1.8.3 Middleware
A Middleware is a convenient way to execute common tasks such as custom authentication before or after a controller
method is being run. You can execute code at the following locations:
before the call of the controller method
after the call of the controller method
after an exception is thrown (also if it is thrown from a middleware, e.g. if an authentication fails)
before the output is rendered
1.8.4 Container
The Container is the place where you define all of your classes and in particular all of your controllers. The container
is responsible for assembling all of your objects (instantiating your classes) that should only have one single instance
without relying on globals or singletons. If you want to know more about why you should use it and what the benefits
are, read up on the topic in Container.
1.8.5 Controller
The controller contains the code that you actually want to run after a request has come in. Think of it like a callback
that is executed if everything before went fine.
The controller returns a response which is then run through the middleware again (afterController and beforeOutput
hooks are being run), HTTP headers are being set and the responses render method is being called and printed.
1.9 Routing
Routes map an URL and a method to a controller method. Routes are defined inside appinfo/routes.php by
passing a configuration array to the registerRoutes method. An example route would look like this:
<?php
namespace OCA\MyApp\AppInfo;
$application = new Application();
$application->registerRoutes($this, array(
'routes' => array(
array('name' => 'page#index', 'url' => '/', 'verb' => 'GET'),
)
));
1.9. Routing
57
use \OCP\AppFramework\App;
use \OCA\MyApp\Controller\PageController;
method (Optional, defaults to GET): The HTTP method that should be matched, (e.g. GET, POST, PUT,
DELETE, HEAD, OPTIONS, PATCH)
requirements (Optional): lets you match and extract URLs that have slashes in them (see Matching suburls)
postfix (Optional): lets you define a route id postfix. Since each route name will be transformed to a route id
(page#method -> myapp.page.method) and the route id can only exist once you can use the postfix option to
alter the route id creation by adding a string to the route id e.g.: name => page#method, postfix => test
will yield the route id myapp.page.methodtest. This makes it possible to add more than one route/url for a
controller method
defaults (Optional): If this setting is given, a default value will be assumed for each url parameter which is not
present. The default values are passed in as a key => value par array
58
}
}
The identifier used inside the route is being passed into controller method by reflecting the method parameters. So
basically if you want to get the value {id} in your method, you need to add $id to your method parameters.
1.9. Routing
59
{
// $page will be 1
}
}
60
Inside the PageController the URL generator can now be used to generate an URL for a redirect:
<?php
namespace OCA\MyApp\Controller;
use
use
use
use
\OCP\IRequest;
\OCP\IURLGenerator;
\OCP\AppFramework\Controller;
\OCP\AppFramework\Http\RedirectResponse;
1.9. Routing
61
/**
* redirect to /apps/news/myapp/authors/3
*/
public function redirect() {
// route name: author_api#do_something
// route url: /apps/news/myapp/authors/{id}
// # needs to be replaced with a . due to limitations and prefixed
// with your app id
$route = 'myapp.author_api.do_something';
$parameters = array('id' => 3);
$url = $this->urlGenerator->linkToRoute($route, $parameters);
return new RedirectResponse($url);
}
}
URLGenerator is case sensitive, so appName must match exactly the name you use in configuration. If you use a
CamelCase name as myCamelCaseApp,
<?php
$route = 'myCamelCaseApp.author_api.do_something';
1.10 Middleware
Middleware is logic that is run before and after each request and is modelled after Djangos Middleware system. It
offers the following hooks:
beforeController: This is executed before a controller method is being executed. This allows you to plug
additional checks or logic before that method, like for instance security checks
afterException: This is being run when either the beforeController method or the controller method itself
is throwing an exception. The middleware is asked in reverse order to handle the exception and to return a
response. If the middleware cant handle the exception, it throws the exception again
afterController: This is being run after a successful controllermethod call and allows the manipulation of a
Response object. The middleware is run in reverse order
beforeOutput: This is being run after the response object has been rendered and allows the manipulation of the
outputted text. The middleware is run in reverse order
To generate your own middleware, simply inherit from the Middleware class and overwrite the methods that should
be used.
<?php
namespace OCA\MyApp\Middleware;
use \OCP\AppFramework\Middleware;
62
/**
* this replaces "bad words" with "********" in the output
*/
public function beforeOutput($controller, $methodName, $output){
return str_replace('bad words', '********', $output);
}
}
The middleware can be registered in the Container and added using the registerMiddleware method:
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Middleware\CensorMiddleware;
class MyApp extends App {
/**
* Define your dependencies in here
*/
public function __construct(array $urlParams=array()){
parent::__construct('myapp', $urlParams);
$container = $this->getContainer();
/**
* Middleware
*/
$container->registerService('CensorMiddleware', function($c){
return new CensorMiddleware();
});
// executed in the order that it is registered
$container->registerMiddleware('CensorMiddleware');
}
}
Note: The order is important! The middleware that is registered first gets run first in the beforeController method.
For all other hooks, the order is being reversed, meaning: if a middleware is registered first, it gets run last.
1.10. Middleware
63
use \OCP\AppFramework\Middleware;
use \OCP\AppFramework\Utility\ControllerMethodReflector;
use \OCP\IRequest;
class HeaderMiddleware extends Middleware {
private $reflector;
public function __construct(ControllerMethodReflector $reflector) {
$this->reflector = $reflector;
}
/**
* Add custom header if @MyHeader is used
*/
public function afterController($controller, $methodName, IResponse $response){
if($this->reflector->hasAnnotation('MyHeader')) {
$response->addHeader('My-Header', 3);
}
return $response;
}
}
64
1.11 Container
The App Framework assembles the application by using a container based on the software pattern Dependency Injection. This makes the code easier to test and thus easier to maintain.
If you are unfamiliar with this pattern, watch the following videos:
Dependency Injection and the art of Services and Containers Tutorial
Google Clean Code Talks
1.11. Container
65
The solution for this particular problem is to limit the new AuthorMapper to one file, the container. The container
contains all the factories for creating these objects and is configured in appinfo/application.php.
To add the apps classes simply open the appinfo/application.php use the registerService method on the
container object:
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Controller\AuthorController;
use \OCA\MyApp\Service\AuthorService;
use \OCA\MyApp\Db\AuthorMapper;
class Application extends App {
/**
* Define your dependencies in here
*/
public function __construct(array $urlParams=array()){
parent::__construct('myapp', $urlParams);
$container = $this->getContainer();
/**
* Controllers
*/
$container->registerService('AuthorController', function($c){
return new AuthorController(
$c->query('AppName'),
$c->query('Request'),
$c->query('AuthorService')
);
});
/**
* Services
*/
$container->registerService('AuthorService', function($c){
return new AuthorService(
$c->query('AuthorMapper')
);
});
/**
* Services
*/
$container->registerService('AuthorMapper', function($c){
return new AuthorMapper(
$c->query('ServerContainer')->getDb()
);
});
}
}
66
AuthorMapper is queried:
$container->registerService('AuthorMappers', function($c){
return new AuthorService(
$c->query('ServerContainer')->getDb()
);
});
1.11. Container
67
// true
Note: $AppName is resolved because the container registered a parameter under the key AppName which will return
the app id. The lookup is case sensitive so while $AppName will work correctly, using $appName as a constructor
parameter will fail.
68
If the entry does not exist, the container is queried for OCA\AppName\Controller\PageController and if no entry
exists, the container tries to create the class by using reflection on its constructor parameters
How does this affect controllers
The only thing what needs to be done to add a route and a controller method is now:
myapp/appinfo/routes.php
<?php
return ['routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
]];
myapp/appinfo/controller/pagecontroller.php
<?php
namespace OCA\MyApp\Controller;
class PageController {
public function __construct($AppName, \OCP\IRequest $request) {
parent::__construct($AppName, $request);
}
public function index() {
// your code here
}
}
1.11. Container
69
return $c->query('OCA\MyApp\Db\AuthorMapper');
});
}
}
70
OCP\AppFramework\Utility\ITimeFactory
OCP\ITagManager
OCP\ITempManager
OCP\Route\IRouter
OCP\ISearch
OCP\ISearch
OCP\Security\ICrypto
OCP\Security\IHasher
OCP\Security\ISecureRandom
OCP\IURLGenerator
OCP\IUserManager
OCP\IUserSession
How to enable it
To make use of this new feature, the following things have to be done:
appinfo/info.xml requires to provide another field called namespace where the namespace of the app is
defined. The required namespace is the one which comes after the top level namespace OCA\, e.g.: for
OCA\MyBeautifulApp\Some\OtherClass the needed namespace would be MyBeautifulApp and would be
added to the info.xml in the following way:
<?xml version="1.0"?>
<info>
<namespace>MyBeautifulApp</namespace>
<!-- other options here ... -->
</info>
appinfo/routes.php: Instead of creating a new Application class instance, simply return the routes array like:
<?php
return ['routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
]];
Note: A namespace tag is required because you can not deduce the namespace from the app id
1.11. Container
71
The output does not depend on the input variables (also called impure function), e.g. time, random number
generator
It is a service, basically it would make sense to swap it out for a different object
What not to inject:
It is pure data and has methods that only act upon it (arrays, data objects)
It is a pure function
1.12 Controllers
Controllers are used to connect routes with app logic. Think of it as callbacks that are executed once a request has
come in. Controllers are defined inside the controller/ directory.
To create a controller, simply extend the Controller class and create a method that should be executed on a request:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
class AuthorController extends Controller {
public function index() {
}
}
72
$c->query('AppName'),
$c->query('Request')
);
});
}
}
Every controller needs the app name and the request object passed into their parent constructor, which can easily be
injected like shown in the example code above. The important part is not the class name but rather the string which is
passed in as the first parameter of the registerService method.
The other part is the route name. An example route name would look like this:
author_api#some_method
Split at the # and uppercase the first letter of the left part:
AuthorApi
someMethod
Now retrieve the service listed under AuthorApiController from the container, look up the parameters of
the someMethod method in the request, cast them if there are PHPDoc type annotations and execute the
someMethod method on the controller with those parameters.
1.12. Controllers
73
// this method will be executed with the id and name parameter taken
// from the request
public function doSomething($id, $name) {
}
}
It is also possible to set default parameter values by using PHP default method values so common values can be
omitted:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
class PageController extends Controller {
/**
* @param int $id
*/
public function doSomething($id, $name='john', $job='author') {
// GET ?id=3&job=killer
// $id = 3
// $name = 'john'
// $job = 'killer'
}
}
Casting parameters
URL, GET and application/x-www-form-urlencoded have the problem that every parameter is a string, meaning that:
?doMore=false
would be passed in as the string false which is not what one would expect. To cast these to the correct types, simply
add PHPDoc in the form of:
@param type $name
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
class PageController extends Controller {
/**
* @param int $id
* @param bool $doMore
* @param float $value
74
*/
public function doSomething($id, $doMore, $value) {
// GET /index.php/apps/myapp?id=3&doMore=false&value=3.5
// => $id = 3
//
$doMore = false
//
$value = 3.5
}
}
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
class PageController extends Controller {
public
//
//
//
//
}
75
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\IRequest;
class PageController extends Controller {
public function someMethod() {
$type = $this->request->getHeader('Content-Type'); // $_SERVER['HTTP_CONTENT_TYPE']
$cookie = $this->request->getCookie('myCookie'); // $_COOKIES['myCookie']
$file = $this->request->getUploadedFile('myfile'); // $_FILES['myfile']
$env = $this->request->getEnv('SOME_VAR'); // $_ENV['SOME_VAR']
}
}
Why should those values be accessed from the request object and not from the global array like $_FILES? Simple:
because its bad practice and will make testing harder.
Reading and writing session variables
To set, get or modify session variables, the ISession object has to be injected into the controller.
Then session variables can be accessed like this:
Note: The session is closed automatically for writing, unless you add the @UseSession annotation!
<?php
namespace OCA\MyApp\Controller;
use OCP\ISession;
use OCP\IRequest;
use OCP\AppFramework\Controller;
class PageController extends Controller {
private $session;
public function __construct($AppName, IRequest $request, ISession $session) {
parent::__construct($AppName, $request);
$this->session = $session;
}
/**
* The following annotation is only needed for writing session values
* @UseSession
*/
public function writeASessionVariable() {
// read a session variable
$value = $this->session['value'];
// write a session variable
$this->session['value'] = 'new value';
}
76
Setting cookies
Cookies can be set or modified directly on the response class:
<?php
namespace OCA\MyApp\Controller;
use DateTime;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
class BakeryController extends Controller {
/**
* Adds a cookie "foo" with value "bar" that expires after user closes the browser
* Adds a cookie "bar" with value "foo" that expires 2015-01-01
*/
public function addCookie() {
$response = new TemplateResponse(...);
$response->addCookie('foo', 'bar');
$response->addCookie('bar', 'foo', new DateTime('2015-01-01 00:00'));
return $response;
}
/**
* Invalidates the cookie "foo"
* Invalidates the cookie "bar" and "bazinga"
*/
public function invalidateCookie() {
$response = new TemplateResponse(...);
$response->invalidateCookie('foo');
$response->invalidateCookies(array('bar', 'bazinga'));
return $response;
}
}
1.12.3 Responses
Similar to how every controller receives a request object, every controller method has to to return a Response. This
can be in the form of a Response subclass or in the form of a value that can be handled by a registered responder.
JSON
Returning JSON is simple, just pass an array to a JSONResponse:
<?php
namespace OCA\MyApp\Controller;
1.12. Controllers
77
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
class PageController extends Controller {
public function returnJSON() {
$params = array('test' => 'hi');
return new JSONResponse($params);
}
}
Because returning JSON is such an common task, theres even a shorter way how to do this:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
class PageController extends Controller {
public function returnJSON() {
return array('test' => 'hi');
}
}
Why does this work? Because the dispatcher sees that the controller did not return a subclass of a Response and asks
the controller to turn the value into a Response. Thats where responders come in.
Responders
Responders are short functions that take a value and return a response. They are used to return different kinds of
responses based on a format parameter which is supplied by the client. Think of an API that is able to return both
XML and JSON depending on if you call the URL with:
?format=xml
or:
?format=json
or:
/index.php/apps/myapp/authors.{format}
78
If there is none, take the Accept header, use the first mimetype and cut off application/. In the following example
the format would be xml:
Accept: application/xml, application/json
If there is no Accept header or the responder does not exist, format defaults to json.
By default there is only a responder for JSON but more can be added easily:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
class PageController extends Controller {
public function returnHi() {
// XMLResponse has to be implemented
$this->registerResponder('xml', function($value) {
if ($value instanceof DataResponse) {
return new XMLResponse(
$value->getData(),
$value->getStatus(),
$value->getHeaders()
);
} else {
return new XMLResponse($value);
}
});
return array('test' => 'hi');
}
}
Note: The above example would only return XML if the format parameter was xml. If you want to return an
XMLResponse regardless of the format parameter, extend the Response class and return a new instance of it from the
controller method instead.
New in version 8.
Because returning values works fine in case of a success but not in case of failure that requires a custom HTTP error
code, you can always wrap the value in a DataResponse. This works for both normal responses and error responses.
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Http;
class PageController extends Controller {
public function returnHi() {
try {
1.12. Controllers
79
Templates
A template can be rendered by returning a TemplateResponse. A TemplateResponse takes the following parameters:
appName: tells the template engine in which app the template should be located
templateName: the name of the template inside the template/ folder without the .php extension
parameters: optional array parameters that can is available in the template through $_, e.g.:
array('key' => 'something')
renderAs: defaults to user, tells ownCloud if it should include it in the web interface, or in case blank is passed
solely render the template
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
class PageController extends Controller {
public function index() {
$templateName = 'main'; // will use templates/main.php
$parameters = array('key' => 'hi');
return new TemplateResponse($this->appName, $templateName, $parameters);
}
}
Redirects
A redirect can be achieved by returning a RedirectResponse:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
80
Downloads
A file download can be triggered by returning a DownloadResponse:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\DownloadResponse;
class PageController extends Controller {
public function downloadXMLFile() {
$path = '/some/path/to/file.xml';
$contentType = 'application/xml';
return new DownloadResponse($path, $contentType);
}
}
1.12. Controllers
81
}
}
simply
implement
the
interface
<?php
namespace OCA\MyApp\Http;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\ICallbackResponse;
class LazyResponse extends Response implements ICallbackResponse {
public function callback(IOutput $output) {
// custom code in here
}
}
Note: Because this code is rendered after several usually built in helpers, you need to take care of errors and proper
HTTP caching by yourself.
Note: Double check your content and edge cases before you relax the policy! Also read the documentation provided
by MDN
To relax the policy pass an instance of the ContentSecurityPolicy class to your response. The methods on the class can
be chained.
The following methods turn off security features by passing in true as the $isAllowed parameter
allowInlineScript (bool $isAllowed)
allowInlineStyle (bool $isAllowed)
allowEvalScript (bool $isAllowed)
The following methods whitelist domains by passing in a domain or * for any domain:
addAllowedScriptDomain (string $domain)
addAllowedStyleDomain (string $domain)
addAllowedFontDomain (string $domain)
addAllowedImageDomain (string $domain)
addAllowedConnectDomain (string $domain)
addAllowedMediaDomain (string $domain)
addAllowedObjectDomain (string $domain)
addAllowedFrameDomain (string $domain)
addAllowedChildSrcDomain (string $domain)
The following policy for instance allows images, audio and videos from other domains:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Http\ContentSecurityPolicy;
class PageController extends Controller {
public function index() {
$response = new TemplateResponse('myapp', 'main');
$csp = new ContentSecurityPolicy();
$csp->addAllowedImageDomain('*');
->addAllowedMediaDomain('*');
$response->setContentSecurityPolicy($csp);
}
}
OCS
New in version 8.1.
Note: This is purely for compatibility reasons. If you are planning to offer an external API, go for a RESTful API
1.12. Controllers
83
instead.
In order to ease migration from OCS API routes to the App Framework, an additional controller and response have
been added. To migrate your API you can use the OCP\AppFramework\OCSController baseclass and return your
data in the form of an array in the following way:
<?php
namespace OCA\MyApp\Controller;
use OCP\AppFramework\OCSController;
class ShareController extends OCSController {
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* @CORS
*/
public function getShares() {
return [
'data' => [
// actual data is in here
],
// optional
'statuscode' => 100,
'status' => 'OK'
];
}
}
84
1.12.4 Authentication
By default every controller method enforces the maximum security, which is:
Ensure that the user is admin
Ensure that the user is logged in
Check the CSRF token
Most of the time though it makes sense to also allow normal users to access the page and the PageController->index()
method should not check the CSRF token because it has not yet been sent to the client and because of that cant work.
To turn off checks the following Annotations can be added before the controller:
@NoAdminRequired: Also users that are not admins can access the page
@NoCSRFRequired: Dont check the CSRF token (use this wisely since you might create a security hole, to
understand what it does see Security Guidelines)
@PublicPage: Everyone can access that page without having to log in
A controller method that turns off all checks would look like this:
<?php
namespace OCA\MyApp\Controller;
use OCP\IRequest;
use OCP\AppFramework\Controller;
class PageController extends Controller {
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*/
public function freeForAll() {
}
}
85
<?php
namespace OCA\MyApp\Controller;
use \OCP\AppFramework\ApiController;
use \OCP\IRequest;
class AuthorApiController extends ApiController {
public function __construct($appName, IRequest $request) {
parent::__construct($appName, $request);
}
/**
* @CORS
*/
public function index() {
}
}
CORS also needs a separate URL for the preflighted OPTIONS request that easily be added by adding the following
route:
<?php
// appinfo/routes.php
array(
'name' => 'author_api#preflighted_cors',
'url' => '/api/1.0/{path}',
'verb' => 'OPTIONS',
'requirements' => array('path' => '.+')
)
Keep in mind that multiple apps will likely depend on the API interface once it is published and they will move at
different speeds to react on changes implemented in the API. Therefore it is recommended to version the API in the
URL to not break existing apps when backwards incompatible changes are introduced:
/index.php/apps/myapp/api/1.0/resource
86
use \OCP\IRequest;
class AuthorApiController extends ApiController {
public function __construct($appName, IRequest $request) {
parent::__construct(
$appName,
$request,
'PUT, POST, GET, DELETE, PATCH',
'Authorization, Content-Type, Accept',
1728000);
}
}
1.14 Templates
ownCloud provides its own templating system which is basically plain PHP with some additional functions and preset
variables. All the parameters which have been passed from the controller are available in an array called $_[], e.g.:
array('key' => 'something')
Note: To prevent XSS the following PHP functions for printing are forbidden: echo, print() and <?=. Instead
use the p() function for printing your values. Should you require unescaped printing, double check for XSS and use:
print_unescaped.
Printing values is done by using the p() function, printing HTML is done by using print_unescaped()
templates/main.php
<?php foreach($_['entries'] as $entry){ ?>
<p><?php p($entry); ?></p>
<?php
}
The parent variables will also be available in the included templates, but should you require it, you can also pass new
variables to it by using the second optional parameter as array for $this->inc.
templates/sub.inc.php
1.14. Templates
87
1.15 JavaScript
The JavaScript files reside in the js/ folder and should be included in the template:
<?php
// add one file
script('myapp', 'script');
// adds js/script.js
//
The recommended JavaScript framework to use is AngularJS. A nice tutorial screencast collection can be found on
Egghead.io
88
1.16 CSS
The CSS files reside in the css/ folder and should be included in the template:
<?php
// include one file
style('myapp', 'style');
// adds js/style.css
Web Components go into the component/ folder and can be imported like this:
<?php
// include one file
component('myapp', 'tabs');
// adds component/tabs.html
Note: Keep in mind that Web Components are still very new and you might need to add polyfills using Polymer
1.16. CSS
89
For built in mobile support your content has to be wrapped inside another div with the id app-content-wrapper.
1.16.2 Navigation
ownCloud provides a default CSS navigation layout. If list entries should have 16x16 px icons, the with-icon class
can be added to the base ul. The maximum supported indention level is two, further indentions are not recommended.
<div id="app-navigation">
<ul class="with-icon">
<li><a href="#">First level entry</a></li>
<li>
<a href="#">First level container</a>
<ul>
<li><a href="#">Second level entry</a></li>
<li><a href="#">Second level entry</a></li>
</ul>
</li>
</ul>
</div>
Folders
Folders are like normal entries and are only supported for the first level. In contrast to normal entries, the links which
show the title of the folder need to have the icon-folder css class.
If the folder should be collapsible, the collapsible class and a button with the class collapse are needed. After adding
the collapsible class the folders child entries can be toggled by adding the open class to the list element:
<div id="app-navigation">
<ul class="with-icon">
<li><a href="#">First level entry</a></li>
<li class="collapsible open">
<button class="collapse"></button>
<a href="#" class="icon-folder">Folder name</a>
<ul>
<li><a href="#">Folder contents</a></li>
<li><a href="#">Folder contents</a></li>
</ul>
</li>
</ul>
</div>
90
<ul>
<li><a href="#">Folder contents</a></li>
<li><a href="#">Folder contents</a></li>
</ul>
</li>
</ul>
</div>
Menus
New in version 8.
To add actions that affect the current list element you can add a menu for second and/or first level elements by adding
the button and menu inside the corresponding li element and adding the with-menu css class:
<div id="app-navigation">
<ul>
<li class="with-counter with-menu">
<a href="#">First level entry</a>
<div class="app-navigation-entry-utils">
<ul>
<li class="app-navigation-entry-utils-counter">15</li>
<li class="app-navigation-entry-utils-menu-button svg"><button></button></li>
</ul>
</div>
<div class="app-navigation-entry-menu open">
<ul>
<li><button class="icon-rename svg" title="rename"></button></li>
<li><button class="icon-delete svg" title="delete"></button></li>
</ul>
</div>
</li>
</div>
The div with the class app-navigation-entry-utils contains only the button (class: app-navigation-entry-utils-menubutton) to display the menu but in many cases another entry is needed to display some sort of count (mails count,
unread feed count, etc.). In that case add the with-counter class to the list entry to adjust the correct padding and
text-oveflow of the entrys title.
The count should be limitted to 999 and turn to 999+ if any higher number is given. If AngularJS is used the following
filter can be used to get the correct behaviour:
app.filter('counterFormatter', function () {
'use strict';
return function (count) {
if (count > 999) {
return '999+';
}
return count;
};
});
1.16. CSS
91
The menu is hidden by default (display: none) and has to be triggered by adding the open class to the app-navigationentry-menu div.
In case of AngularJS the following small directive can be added to handle all the display and click logic out of the box:
app.run(function ($document, $rootScope) {
'use strict';
$document.click(function (event) {
$rootScope.$broadcast('documentClicked', event);
});
});
app.directive('appNavigationEntryUtils', function () {
'use strict';
return {
restrict: 'C',
link: function (scope, elm) {
var menu = elm.siblings('.app-navigation-entry-menu');
var button = $(elm)
.find('.app-navigation-entry-utils-menu-button button');
button.click(function () {
menu.toggleClass('open');
});
scope.$on('documentClicked', function (scope, event) {
if (event.target !== button[0]) {
menu.removeClass('open');
}
});
}
};
});
Editing
New in version 8.
Often an edit option is needed an entry. To add one for a given entry simply hide the title and add the following div
inside the entry:
<div id="app-navigation">
<ul class="with-icon">
<li>
<a href="#" class="hidden">First level entry</a>
<div class="app-navigation-entry-edit">
<form>
<input type="text" value="First level entry" autofocus-on-insert>
<input type="submit" value="" class="action icon-checkmark">
</form>
</div>
92
</li>
</ul>
</div>
If AngularJS is used you want to autofocus the input box. This can be achieved by placing the show condition inside
an ng-if on the app-navigation-entry-edit div and adding the following directive:
app.directive('autofocusOnInsert', function () {
'use strict';
return function (scope, elm) {
elm.focus();
};
});
ng-if is required because it removes/inserts the element into the DOM dynamically instead of just adding a display:
none to it like ng-show and ng-hide.
Undo entry
New in version 8.
If you want to undo a performed action on a navigation entry such as deletion, you should show the undo directly in
place of the entry and make it disappear after location change or 7 seconds:
<div id="app-navigation">
<ul class="with-icon">
<li>
<a href="#" class="hidden">First level entry</a>
<div class="app-navigation-entry-deleted">
<div class="app-navigation-entry-deleted-description">Deleted X</div>
<button class="app-navigation-entry-deleted-button icon-history" title="Undo"></butt
</div>
</li>
</ul>
</div>
1.16. CSS
93
</div>
<div id="app-settings-content">
<!-- Your settings in here -->
</div>
</div>
</div>
</div>
The data attribute data-apps-slide-toggle slides up a target area using a jQuery selector and hides the area if the user
clicks outside of it.
1.16.4 Icons
To use icons which are shipped in core, special class to apply the background image are supplied. All of these classes
use background-position: center and background-repeat: no-repeat.
icon-breadcrumb:
icon-loading:
icon-loading-dark:
icon-loading-small:
icon-add:
icon-caret:
icon-caret-dark:
icon-checkmark:
icon-checkmark-white:
icon-clock:
icon-close:
icon-confirm:
icon-delete:
icon-download:
icon-history:
icon-info:
icon-lock:
94
icon-logout:
icon-mail:
icon-more:
icon-password:
icon-pause:
icon-pause-big:
icon-play:
icon-play-add:
icon-play-big:
icon-play-next:
icon-play-previous:
icon-public:
icon-rename:
icon-search:
icon-settings:
icon-share:
icon-shared:
icon-sound:
icon-sound-off:
icon-star:
icon-starred:
icon-toggle:
icon-triangle-e:
icon-triangle-n:
icon-triangle-s:
icon-upload:
1.16. CSS
95
icon-upload-white:
icon-user:
icon-view-close:
icon-view-next:
icon-view-pause:
icon-view-play:
icon-view-previous:
icon-calendar-dark:
icon-contacts-dark:
icon-file:
icon-files:
icon-folder:
icon-filetype-text:
icon-filetype-folder:
icon-home:
icon-link:
icon-music:
icon-picture:
96
1.17 Translation
ownClouds translation system is powered by Transifex. To start translating sign up and enter a group. If your
community app should be added to Transifex contact someone of the core developers to set it up for you.
1.17.1 PHP
Should it ever be needed to use localized strings on the server-side, simply inject the L10N service from the ServerContainer into the needed constructor
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Service\AuthorService;
class AuthorService {
private $trans;
public function __construct(IL10N $trans){
$this->trans = $trans;
}
1.17. Translation
97
1.17.2 Templates
In every template the global variable $l can be used to translate the strings using its methods t() and n():
<div><?php p($l->t('Showing %s files', $_['count'])); ?></div>
<button><?php p($l->t('Hide')); ?></button>
1.17.3 JavaScript
There is currently no good way to translate JavaScript strings. One way to still use translated strings in the scripts is
to create an invisible HTML element with all the translations in it which can be parsed in the JavaScript code:
<ul id="translations">
<li id="add-new"><?php p($l->t('Add new file')); ?></li>
</ul>
1.17.4 Hints
In case some translation strings may be translated wrongly because they have multiple meanings, you can add hints
which will be shown in the Transifex web-interface:
<ul id="translations">
<li id="add-new">
<?php
98
// TRANSLATORS Will be shown inside a popup and asks the user to add a new file
p($l->t('Add new file'));
?>
</li>
</ul>
The above script generates a template that can be used to translate all strings of an app. This template is located in the
folder template/ with the name myapp.pot. It can be used by your favored translation tool which then creates a
.po file. The .po file needs to be place in a folder named like the language code with the app name as filename - for
example l10n/es/myapp.po. After this step the perl script needs to be invoked to transfer the po file into our own
fileformat that is more easily readable by the server code:
perl l10n.pl write myapp
You then just need the .php, .json and .js files for a working localized app.
99
To update the tables used by the app, simply adjust the database.xml file and increase the app version number in
appinfo/version to trigger an update.
100
private $db;
public function __construct(IDBConnection $db) {
$this->db = $db;
}
public function find($id) {
$sql = 'SELECT * FROM `*PREFIX*myapp_authors` ' .
'WHERE `id` = ?';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1, $id, \PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch();
$stmt->closeCursor();
return $row;
}
}
1.19.1 Mappers
The aforementioned example is the most basic way write a simple database query but the more queries amass, the
more code has to be written and the harder it will become to maintain it.
To generalize and simplify the problem, split code into resources and create an Entity and a Mapper class for it. The
mapper class provides a way to run SQL queries and maps the result onto the related entities.
To create a mapper, inherit from the mapper baseclass and call the parent constructor with the following parameters:
Database connection
Table name
Optional: Entity class name, defaults to \OCA\MyApp\Db\Author in the example below
<?php
// db/authormapper.php
namespace OCA\MyApp\Db;
use OCP\IDBConnection;
use OCP\AppFramework\Db\Mapper;
class AuthorMapper extends Mapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'myapp_authors');
}
/**
* @throws \OCP\AppFramework\Db\DoesNotExistException if not found
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
*/
101
Note: The cursor is closed automatically for all INSERT, DELETE, UPDATE queries and when calling the methods
findOneQuery, findEntities, findEntity, delete, insert and update. For custom calls using execute you should
always close the cursor after you are done with the fetching to prevent database lock problems on SqLite
Every mapper also implements default methods for deleting and updating an entity based on its id:
$authorMapper->delete($entity);
or:
$authorMapper->update($entity);
1.19.2 Entities
Entities are data objects that carry all the tables information for one row. Every Entity has an id field by default that
is set to the integer type. Table rows are mapped from lower case and underscore separated names to pascal case
attributes:
Table column name: phone_number
Property name: phoneNumber
<?php
// db/author.php
namespace OCA\MyApp\Db;
use OCP\AppFramework\Db\Entity;
class Author extends Entity {
102
protected $stars;
protected $name;
protected $phoneNumber;
public function __construct() {
// add types in constructor
$this->addType('stars', 'integer');
}
}
Types
The following properties should be annotated by types, to not only assure that the types are converted correctly for
storing them in the database (e.g. PHP casts false to the empty string which fails on postgres) but also for casting them
when they are retrieving from the database.
The following types can be added for a field:
integer
float
boolean
Accessing attributes
Since all attributes should be protected, getters and setters are automatically generated for you:
<?php
// db/author.php
namespace OCA\MyApp\Db;
use OCP\AppFramework\Db\Entity;
class Author extends Entity {
protected $stars;
protected $name;
protected $phoneNumber;
}
$author = new Author();
$author->setId(3);
$author->getPhoneNumber()
// null
103
<?php
// db/author.php
namespace OCA\MyApp\Db;
use OCP\AppFramework\Db\Entity;
class Author extends Entity {
protected $stars;
protected $name;
protected $phoneNumber;
// map attribute phoneNumber to the database column phonenumber
public function columnToProperty($column) {
if ($column === 'phonenumber') {
return 'phoneNumber';
} else {
return parent::columnToProperty($column);
}
}
public function propertyToColumn($property) {
if ($column === 'phoneNumber') {
return 'phonenumber';
} else {
return parent::propertyToColumn($property);
}
}
}
Slugs
Slugs are used to identify resources in the URL by a string rather than integer id. Since the URL allows only certain
values, the entity baseclass provides a slugify method for it:
<?php
$author = new Author();
$author->setName('Some*thing');
$author->slugify('name'); // Some-thing
1.20 Configuration
The config allows the app to set global, app and user settings can be injected from the ServerContainer. All values are
saved as strings and must be casted to the correct value.
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Service\AuthorService;
104
class AuthorService {
private $config;
private $appName;
public function __construct(IConfig $config, $appName){
$this->config = $config;
$this->appName = $appName;
}
public function getSystemValue($key) {
return $this->config->getSystemValue($key);
}
public function setSystemValue($key, $value) {
$this->config->setSystemValue($key, $value);
}
}
1.20. Configuration
105
class AuthorService {
private $config;
private $appName;
public function __construct(IConfig $config, $appName){
$this->config = $config;
$this->appName = $appName;
}
public function getAppValue($key) {
return $this->config->getAppValue($this->appName, $key);
}
public function setAppValue($key, $value) {
$this->config->setAppValue($this->appName, $key, $value);
}
}
class AuthorService {
private $config;
private $appName;
public function __construct(IConfig $config, $appName){
$this->config = $config;
$this->appName = $appName;
}
public function getUserValue($key, $userId) {
return $this->config->getUserValue($userId, $this->appName, $key);
}
public function setUserValue($key, $userId, $value) {
$this->config->setUserValue($userId, $this->appName, $key, $value);
106
}
}
1.21 Filesystem
Because users can choose their storage backend, the filesystem should be accessed by using the appropriate filesystem
classes.
Filesystem classes can be injected from the ServerContainer by calling the method getRootFolder(), getUserFolder()
or getAppFolder():
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Storage\AuthorStorage;
1.21. Filesystem
107
private $storage;
public function __construct($storage){
$this->storage = $storage;
}
public function writeTxt($content) {
// check if file exists and write to it if possible
try {
try {
$file = $this->storage->get('/myfile.txt');
} catch(\OCP\Files\NotFoundException $e) {
$this->storage->touch('/myfile.txt');
$file = $this->storage->get('/myfile.txt');
}
// the id can be accessed by $file->getId();
$file->putContent($content);
} catch(\OCP\Files\NotPermittedException $e) {
// you have to create this exception by yourself ;)
throw new StorageException('Cant write to file');
}
}
}
108
1.22 Usermanagement
Users can be managed using the UserManager which is injected from the ServerContainer:
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Service\UserService;
1.22. Usermanagement
109
110
parent::__construct('myapp', $urlParams);
$container = $this->getContainer();
/**
* Controllers
*/
$container->registerService('UserService', function($c) {
return new UserService(
$c->query('UserSession')
);
});
$container->registerService('UserSession', function($c) {
return $c->query('ServerContainer')->getUserSession();
});
// currently logged in user, userId can be gotten by calling the
// getUID() method on it
$container->registerService('User', function($c) {
return $c->query('UserSession')->getUser();
});
}
}
1.23 Hooks
Hooks are used to execute code before or after an event has occurred. This is for instance useful to run cleanup code
after users, groups or files have been deleted. Hooks should be registered in the app.php:
1.23. Hooks
111
<?php
namespace OCA\MyApp\AppInfo;
$app = new Application();
$app->getContainer()->query('UserHooks')->register();
The hook logic should be in a separate class that is being registered in the Container
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Hooks\UserHooks;
<?php
namespace OCA\MyApp\Hooks;
class UserHooks {
private $userManager;
public function __construct($userManager){
$this->userManager = $userManager;
}
public function register() {
$callback = function($user) {
// your code that executes before $user is deleted
};
$userManager->listen('\OC\User', 'preDelete', $callback);
}
}
112
Hooks can also be removed by using the removeListener method on the object:
<?php
// delete previous callback
$userManager->removeListener(null, null, $callback);
113
114
The class for the above example would live in cron/sometask.php. Try to keep the method as small as possible
because its hard to test static methods. Simply reuse the app container and execute a service that was registered in it.
<?php
namespace OCA\MyApp\Cron;
use \OCA\MyApp\AppInfo\Application;
class SomeTask {
public static function run() {
$app = new Application();
$container = $app->getContainer();
$container->query('SomeService')->run();
}
}
* php -f /srv/http/owncloud/cron.php
1.25 Logging
The logger can be injected from the ServerContainer:
<?php
namespace OCA\MyApp\AppInfo;
use \OCP\AppFramework\App;
use \OCA\MyApp\Service\AuthorService;
115
/**
* Controllers
*/
$container->registerService('AuthorService', function($c) {
return new AuthorService(
$c->query('Logger'),
$c->query('AppName')
);
});
$container->registerService('Logger', function($c) {
return $c->query('ServerContainer')->getLogger();
});
}
}
class AuthorService {
private $logger;
private $appName;
public function __construct(ILogger $logger, $appName){
$this->logger = $logger;
$this->appName = $appName;
}
public function log($message) {
$this->logger->error($message, array('app' => $this->appName));
}
}
116
1.26 Testing
All PHP classes can be tested with PHPUnit, JavaScript can be tested by using Karma.
1.26.1 PHP
The PHP tests go into the tests/ directory. Unfortunately the classloader in core requires a running server (as in fully
configured and setup up with a database connection). This is unfortunately too complicated and slow so a separate
classloader has to be provided. If the app has been generated with the ocdev startapp command, the classloader is
already present in the the tests/ directory and PHPUnit can be run with:
phpunit tests/
PHP classes should be tested by accessing them from the container to ensure that the container is wired up properly.
Services that should be mocked can be replaced directly in the container.
A test for the AuthorStorage class in Filesystem:
<?php
namespace OCA\MyApp\Storage;
class AuthorStorage {
private $storage;
public function __construct($storage){
$this->storage = $storage;
}
public function getContent($id) {
// check if file exists and write to it if possible
try {
$file = $this->storage->getById($id);
if($file instanceof \OCP\Files\File) {
return $file->getContent();
} else {
throw new StorageException('Can not read from folder');
}
} catch(\OCP\Files\NotFoundException $e) {
throw new StorageException('File does not exist');
}
}
}
1.26. Testing
117
Make sure to extend the \Test\TestCase class with your test and always call the parent methods, when overwriting setUp(), setUpBeforeClass(), tearDown() or tearDownAfterClass() method from the TestCase. These methods set up important stuff and clean up the system after the test, so the next test can run without side
effects, like remaining files and entries in the file cache, etc.
118
119
Hooks
Listen on events like user creation and execute code:
Hooks
Background Jobs
Periodically run code in the background:
Background Jobs (Cron)
Logging
Log to the data/owncloud.log:
Logging
Testing
Write automated tests to ensure stability and ease maintenance:
Testing
PHPDoc Class Documentation
ownCloud class and function documentation:
ownCloud App API
120
The ownCloud Android library may be obtained from the following Github repository:
https://github.com/owncloud/android-library
Once obtained, this code should be compiled. The Github repository not only contains the library, but also a sample
project, sample_client sample_client properties/android/librerias , which will assist in learning how to use the library.
Add the library to a project
There are different methods to add an external library to a project, then we will describe one of them.
1. Compile the ownCloud Android Library
2. Define a dependency within your project.
For that, access to Properties > Android > Library ** ** and click on add and select the ownCloud Android library
121
Then all the public classes and methods of the library will be available for your own app.
Examples
Init the library
Start using the library; it is needed to init the object mClient that will be in charge of keeping the communication with
the server.
Code example
public class MainActivity extends Activity
implements OnRemoteOperationListener,
OnDatatransferProgressListener {
122
Set credentials
Create a folder
Create a new folder on the cloud server, the info needed to be sent is the path of the new folder.
123
Code example
Read folder
Get the content of an existing folder on the cloud server, the info needed to be sent is the path of the folder, in the
example shown it has been asked the content of the root folder. As answer of this method, it will be received an array
with all the files and folders stored in the selected folder.
Code example
@Override
public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
if (operation instanceof ReadRemoteFolderOperation) {
if (result.isSuccess()) {
List< RemoteFile > files = result.getData();
// do your stuff here
}
}
...
}
Read file
Get information related to a certain file or folder, information obtained is: filePath, filename, isDirectory,
size and date.
Code example
private void startReadFileProperties(String filePath) {
ReadRemoteFileOperation readOperation = new ReadRemoteFileOperation(filePath);
readOperation.execute(mClient, this, mHandler);
124
}
@Override
public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
if (operation instanceof ReadRemoteFileOperation) {
if (result.isSuccess()) {
RemoteFile file = result.getData()[0];
// do your stuff here
}
}
...
}
Delete a file or folder on the cloud server. The info needed is the path of folder/file to be deleted.
Code example
private void startRemoveFile(String filePath) {
RemoveRemoteFileOperation removeOperation = new RemoveRemoteFileOperation(remotePath);
removeOperation.execute( mClient , this , mHandler);
}
@Override
public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationResult result) {
if (operation instanceof RemoveRemoteFileOperation) {
if (result.isSuccess()) {
// do your stuff here
}
}
...
}
Download a file
Download an existing file on the cloud server. The info needed is path of the file on the server and targetDirectory,
path where the file will be stored on the device.
Code example
125
}
}
}
@Override
public void onTransferProgress( long progressRate, long totalTransferredSoFar, long totalToTransfer,
mHandler.post( new Runnable() {
@Override
public void run() {
// do your UI updates about progress here
}
});
}
Upload a file
Upload a new file to the cloud server. The info needed is fileToUpload, path where the file is stored on the device,
remotePath, path where the file will be stored on the server and mimeType.
Code example
Move an exisintg file or folder to a different location in the ownCloud server. Parameters needed are the path to the
file or folder to move, and the new path desired for it. The parent folder of the new path must exist in the server.
When the parameter overwrite is set to true, the file or folder is moved even if the new path is already used by a
different file or folder. This one will be replaced by the former.
126
Code example
Get information about what files and folder are shared by link (the object mClient contains the information about the
server url and account)
Code example
private void startAllSharesRetrieval() {
GetRemoteSharesOperation getSharesOp = new GetRemoteSharesOperation();
getSharesOp.execute( mClient , this , mHandler);
}
@Override
public void onRemoteOperationFinish( RemoteOperation operation, RemoteOperationResult result) {
if (operation instanceof GetRemoteSharesOperation) {
if (result.isSuccess()) {
ArrayList< OCShare > shares = new ArrayList< OCShare >();
for (Object obj: result.getData()) {
shares.add(( OCShare) obj);
}
// do your stuff here
}
}
}
Get information about what files and folder are shared by link on a certain folder. The info needed is filePath, path of
the file/folder on the server, the Boolean variable, getReshares, come from the Sharing api, from the moment it is not
in use within the ownCloud Android library.
Code example
127
128
Tips
129
Note that contribution to the iOS client require signing the iOS addendum to the ownCloud Contributor Agreement.
You are permitted to test the iOS client on Apple hardware thanks to the iOS license exception.
The ownCloud iOS library may be obtained from the following Github repository:
[email protected]:owncloud/ios-library.git
Once obtained, this code should be compiled with Xcode 6. The Github repository not only contains the library,
ownCloud iOS library, but also contains a sample project, OCLibraryExample, which will assist in learning how to
use the library.
Add the library to a project
130
2. Add the library file to the project. From the Build Phases tab, scroll to Link binary files and select the + to
add a library. Select the library file.
3. Add the path of the library header files. Under the Build Settings tab, select the target library and add the path in
the Header Search Paths field.
131
4. Remaining in the Build Setting tab, add the flag -Obj-C under the Other Linker Flags option.
At this stage, the library is included on your project and you can start communicating with the ownCloud server.
132
Include the library as a subproject Follow these steps if this is the desired method.
5. Add the file ownCloud iOS library.xcodeproj to the project via drag and drop.
6. Within the project, navigate to the Build Phases tab. Under the Target Dependencies section, select the + and
choose the library target.
133
7. Link the library file to the project target. Under the Build Phases tab, select the + under the Link Binary with
Libraries section and select the library file.
134
8. Add the flag -Obj-C to Other Linker Flags under the project target on the Build Settings tab.
9. Finally add the path of the library headers. Under the Build Settings tab, add the path under the Header Search
Paths option.
Sources
135
Also could happen that you need to overwrite the class AFURLSessionManager to manage SSL Certificates
#import "OCCommunication.h"
+ (OCCommunication*)sharedOCCommunication
{
static OCCommunication* sharedOCCommunication = nil;
if (sharedOCCommunication == nil)
{
//Network Upload queue for NSURLSession (iOS 7)
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigura
configuration.HTTPMaximumConnectionsPerHost = 1;
configuration.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
OCURLSessionManager *uploadSessionManager = [[OCURLSessionManager alloc] initWithSessionConfigur
[uploadSessionManager.operationQueue setMaxConcurrentOperationCount:1];
[uploadSessionManager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChalleng
return NSURLSessionAuthChallengePerformDefaultHandling;
}];
Set credentials
136
Create a folder
Create a new folder on the cloud server, the info needed to be sent is the path of the new folder.
Code example
137
if (error.code == OCErrorForbidenCharacters) {
//Forbidden characters
}
else
{
//Other error
}
}];
Read folder
Get the content of an existing folder on the cloud server, the info needed to be sent is the path of the folder. As answer
of this method, it will be received an array with all the files and folders stored in the selected folder.
Code example
Read file
Get information related to a certain file or folder. Although, more information can be obtained, the library only gets
the eTag.
Other properties of the file or folder may be obtained: filePath, filename, isDirectory, size and date
138
Code example
Move a file or folder from their current path to a new one on the cloud server. The info needed is the origin path and
the destiny path.
Code example
139
}
}
errorBeforeRequest :^( NSError *error) {
if (error.code == OCErrorMovingTheDestinyAndOriginAreTheSame) {
//The destiny and the origin are the same
}
else if (error.code == OCErrorMovingFolderInsideHimself) {
//Moving folder inside himself
}
else if (error.code == OCErrorMovingDestinyNameHaveForbiddenCharacters) {
//Forbidden Characters
}
else
{
//Default
}
}];
Delete a file or folder on the cloud server. The info needed is the path to delete.
Code example
[[ AppDelegate sharedOCCommunication ] deleteFileOrFolder :path onCommunication :[ AppDelegate
sharedOCCommunication ] successRequest :^( NSHTTPURLResponse *response, NSString *redirectedServer)
//File or Folder deleted
}
failureRequest :^( NSHTTPURLResponse *response, NSError *error) {
switch (response.statusCode) {
case kOCErrorServerPathNotFound:
//Path not found
break;
case kOCErrorServerUnauthorized:
//Bad credentials
break;
case kOCErrorServerForbidden:
//Forbidden
break;
case kOCErrorServerTimeout:
//Timeout
break;
default:
break;
}
}];
140
Download a file
Download an existing file on the cloud server. The info needed is the server URL, path of the file on the server and
localPath, path where the file will be stored on the device and a boolean to indicate if is neccesary to use LIFO queue
or FIFO.
Code example
progressDownload :^( NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToR
//Calculate percent
float percent = ( float)totalBytesRead / totalBytesExpectedToRead;
NSLog ( @"Percent of download: %f" , percent); }
successRequest :^(NSHTTPURLResponse *response, NSString *redirectedServer) {
//Download complete
}
failureRequest :^(NSHTTPURLResponse *response, NSError *error) {
switch (response. statusCode) {
case kOCErrorServerUnauthorized:
//Bad credentials
break;
case kOCErrorServerForbidden:
//Forbidden
break;
case kOCErrorProxyAuth:
//Proxy access required
break;
case kOCErrorServerPathNotFound:
//Path not found
break;
default:
//Default
break;
}
}
shouldExecuteAsBackgroundTaskWithExpirationHandler :^{
[op cancel ];
}];
Download an existing file storaged on the cloud server using background session, only supported by iOS 7 and higher.
The info needed is, the server URL: path where the file is stored on the server; localPath: path where the file will be
stored on the device; and NSProgress: object where get the callbacks of the upload progress.
To get the callbacks of the progress is needed use a KVO in the progress object. We add the code in this example of
the call to set the KVO and the method where catch the notifications.
Code example
141
142
Method to set callbacks of the pending download transfers when the app starts. Its used when there are pendings
download background transfers. The block is executed when a pending background task finishes.
Code example
}];
Method to set progress callbacks of the pending download transfers. Its used when there are pendings background
download transfers. The block is executed when a pending task get a input porgress.
Code example
}];
Upload a file
Upload a new file to the cloud server. The info needed is localPath, path where the file is stored on the device and
server URL, path where the file will be stored on the server.
Code example
progressUpload :^( NSUInteger bytesWrote, long long totalBytesWrote, long long totalBytesExpectedToW
//Calculate upload percent
if ( totalBytesExpectedToRead/1024 != 0) {
if ( bytesWrote > 0) {
float percent = totalBytesWrote* 100 / totalBytesExpectedToRead;
NSLog ( @"Percent: %f" , percent);
}
}
}
successRequest :^( NSHTTPURLResponse *response, NSString *redirectedServer) {
//Upload complete
}
failureRequest :^( NSHTTPURLResponse *response, NSString *redirectedServer, NSError *error) {
switch (response. statusCode) {
case kOCErrorServerUnauthorized :
//Bad credentials
break;
case kOCErrorServerForbidden:
143
//Forbidden
break;
case kOCErrorProxyAuth:
//Proxy access required
break;
case kOCErrorServerPathNotFound:
//Path not found
break;
default:
//Default
break;
}
}
failureBeforeRequest :^( NSError *error) {
switch (error.code) {
case OCErrorFileToUploadDoesNotExist:
//File does not exist
break;
default:
//Default
break;
}
}
shouldExecuteAsBackgroundTaskWithExpirationHandler :^{
[op cancel];
}];
Upload a new file to the cloud server using background session, only supported by iOS 7 and higher.
The info needed is localPath, path where the file is stored on the device and server URL, path where the file will be
stored on the server and NSProgress object where get the callbacks of the upload progress.
To get the callbacks of the progress is needed use a KVO in the progress object. We add the code in this example of
the call to set the KVO and the method where catch the notifications.
Code example
NSURLSessionUploadTask *uploadTask = nil;
NSProgress *progress = nil;
144
case kOCErrorServerPathNotFound:
//Path not found
break;
default:
//Default
break;
}
}];
Method to set callbacks of the pending transfers when the app starts. Its used when there are pendings background
transfers. The block is executed when a pending background task finished.
Code example
}];
Method to set progress callbacks of the pending transfers. Its used when there are pendings background transfers.
The block is executed when a pending task get a input porgress.
Code example
145
}];
The Sharing API is included in ownCloud 5.0.13 and greater versions. The info needed is activeUser.url, the server
URL that you want to check.
Code Example
Get information about what files and folder are shared by link.
The info needed is Path, the server URL that you want to check.
Code example
Get information about what files and folder are shared by link in a specific path.
The info needed is the server URL that you want to check and the specific path tha you want to check.
Code example
146
Share a file or a folder from your cloud server by link. The info needed is Path, your server URL and the path of the
item that you want to share (for example /folder/file.pdf)
Code example
147
Tips
148
1.30 Translation
1.30.1 Make text translatable
In HTML or PHP wrap it like this <?php p($l->t(This is some text));?> or this <?php
print_unescaped($l->t(This is some text));?> For the right date format use <?php
p($l->l(date, time()));?>. Change the way dates are shown by editing /core/l10n/l10n-[lang].php To
translate text in javascript use: t(appname,text to translate);
Note: print_unescaped() should be preferred only if you would like to display HTML code. Otherwise, using
p() is strongly preferred to escape HTML characters against XSS attacks.
<?php p($l->t('Select file from')) . ' '; ?><a href='#' id="browselink"><?php p($l->t('local filesys
149
The translation script requires Locale::PO, installable via apt-get install liblocale-po-perl
150
1.31 Unit-Testing
1.31.1 PHP unit testing
Getting PHPUnit
ownCloud uses PHPUnit >= 3.7 for unit testing.
To install it, either get it via your packagemanager:
sudo apt-get install phpunit
or install it manually:
wget https://phar.phpunit.de/phpunit.phar
chmod +x phpunit.phar
sudo mv phpunit.phar /usr/local/bin/phpunit
1.31. Unit-Testing
151
/srv/http/owncloud/apps/myapp/lib/testme.php
<?php
namespace OCA\Myapp;
class TestMe {
public function addTwo($number){
return $number + 2;
}
}
Make sure to extend the \Test\TestCase class with your test and always call the parent methods, when overwriting setUp(), setUpBeforeClass(), tearDown() or tearDownAfterClass() method from the TestCase. These methods set up important stuff and clean up the system after the test, so the next test can run without side
effects, like remaining files and entries in the file cache, etc.
For more resources on PHPUnit visit: http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html
Bootstrapping ownCloud
If you use ownCloud functions or classes in your code, youll need to make them available to your test by bootstrapping
ownCloud.
To do this, youll need to provide the --bootstrap argument when running PHPUnit
/srv/http/owncloud:
phpunit --bootstrap tests/bootstrap.php apps/myapp/tests/testsuite.php
If you run the test under a different user than your web server, youll have to adjust your php.ini and file rights.
/etc/php/php.ini:
open_basedir = none
/srv/http/owncloud:
152
To run a specific test suite (note that the test file path is relative to the tests directory):
./autotest.sh sqlite lib/share/share.php
Further Reading
http://googletesting.blogspot.de/2008/08/by-miko-hevery-so-you-decided-to.html
http://www.phpunit.de/manual/current/en/writing-tests-for-phpunit.html
http://www.youtube.com/watch?v=4E4672CS58Q&feature=bf_prev&list=PLBDAB2BA83BB6588E
Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin)
1.31. Unit-Testing
153
154
In owncloud standard theme everything is held very simple. This allows you quick adapting. In an unchanged ownCloud version CSS files and the standard pictures reside in /owncloud/themes/default folder. The next thing you should
do, before starting any changes is: Make a backup of your current theme(s) e.g.:
Goto . . . /owncloud/themes
cp -r default default.old
1.34 Structure
The folder structure of a theme is exactly the same as the main ownCloud structure. You can override js files, images
and templates with own versions. CSS files are loaded additionally to the default files so you can override CSS
properties.
155
/* HEADERS */
...
background: #1d2d42; /* Old browsers */
background: -moz-linear-gradient(top, #33537a 0%, #1d2d42 100%); /* FF3.6+ */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F1B3A4), color-stop(100%,
background: -webkit-linear-gradient(top, #33537a 0%,#1d2d42 100%); /* Chrome10+,Safari5.1+ */
background: -o-linear-gradient(top, #33537a 0%,#1d2d42 100%); /* Opera11.10+ */
background: -ms-linear-gradient(top, #33537a 0%,#1d2d42 100%); /* IE10+ */
background: linear-gradient(top, #33537a 0%,#1d2d42 100%); /* W3C */
The different background-assignments indicate the headers for a lot of different browser types. What you most likely
want to do is change the #35537a (lighter blue) and #ld2d42 (dark blue) color to the colours of our choice. In
some older and other browsers, there is just one color, but in the rest showing gradients is possible. The login page
background is a horizontal gradient. The first hex number, #35537a, is the top color of the gradient at the login screen.
The second hex number, #ld2d42 is the bottom color of the gradient at the login screen. The gradient in top of the
normal view after login is also defined by these CSS-settings, so that they take effect in logged in situation as well.
Change these colors to the hex color of your choice: As usual:
the first two figures give the intensity of the red channel,
the second two give the green intensity and the
third pair gives the blue value.
Save your CSS-file and refresh to see the new login screen. The other major color scheme is the blue header bar on
the main navigation page once you log in to ownCloud. This color we will change with the above as well. Save the
file and refresh the browser for the changes to take effect.
/* Define the salt used to hash the user passwords. All your user passwords are lost if you lose thi
"passwordsalt" => "",
/* Force use of HTTPS connection (true = use HTTPS) */
"forcessl" => false,
/* Theme to use for ownCloud */
157
158
/* The directory where the user data is stored, default to data in the owncloud
* directory. The sqlite database is also stored here, when sqlite is used.
*/
// "datadirectory" => "",
"apps_paths" => array(
ownCloud will use the first app directory which it finds in the array with writable set to true.
159
1.39.2 Usage
Registering Methods
Methods are registered inside the appinfo/routes.php using OCP\API
<?php
\OCP\API::register(
'get',
'/apps/yourapp/url',
function($urlParameters) {
return new \OC_OCS_Result($data);
},
'yourapp',
\OC_API::ADMIN_AUTH
);
Returning Data
Once the API backend has matched your URL, your callable function as defined in $action will be executed. This
method is passed as array of parameters that you defined in $url. To return data back the the client, you should return
an instance of OC_OCS_Result. The API backend will then use this to construct the XML or JSON response.
Authentication & Basics
Because REST is stateless you have to send user and password each time you access the API. Therefore running
ownCloud with SSL is highly recommended otherwise everyone in your network can log your credentials:
https://user:[email protected]/ocs/v1.php/apps/yourapp
Output
The output defaults to XML. If you want to get JSON append this to the URL:
?format=json
160
</data>
</ocs>
JSON:
{
"ocs": {
"meta": {
"status": "ok",
"statuscode": 100,
"message": null
},
"data": {
// data here
}
}
}
Statuscodes
The statuscode can be any of the following numbers:
100 - successful
996 - server error
997 - not authorized
998 - not found
999 - unknown error
161
162
163
For further questions or help you can also send a mail to:
[email protected] (IRC: dragotin)
[email protected] (IRC: Raydiation)
165
1.43 Bugtracker
1.43.1 Code Reviews on GitHub
Given enough eyeballs, all bugs are shallow
Linus Law
Introduction
In order to increase the code quality within ownCloud, developers are requested to perform code reviews. As we are
now heavily using the GitHub platform these code review shall take place on GitHub as well.
Precondition
From now on no direct commits/pushes to master or any of the stable branches are allowed in general. Every code
change - even one liners - have to be reviewed!
How will it work?
1. A developer will submit his changes on GitHub via a pull request (PR). GitHub:help - using pull requests
2. Within the pull request the developer could already name other developers (using @GitHubusername) and ask
them for review.
3. Using Labels section on the right side, they add 5 - To review label if the patch is complete. If they have no
permission to do that, other developers may add this Label in case PR author had indicated.
4. Other developers (either named or at free will) have a look at the changes and are welcome to write comments
within the comment field.
5. In case the reviewer is okay with the changes and thinks all his comments and suggestions have been take into
account a :+1 on the comment will signal a positive review.
6. Before a pull request will be merged into master or the corresponding branch at least 2 reviewers need to give
:+1 score.
7. Our continuous integration server will give an additional indicator for the quality of the pull request.
Examples
Read our coding guidelines for information on what a good pull request and good ownCloud code looks like.
These are two examples that are considered to be good examples of how pull requests should be handled
https://github.com/owncloud/core/pull/121
https://github.com/owncloud/core/pull/146
Questions?
Feel free to drop a line on the mailing list or join us on IRC.
166
As you may have noticed, the columns of the kanban board represent the life-cycle of an issue (be it a Bug or an
Enhancement). An issue flows from the 1 - Backlog on the left to the 7 - To release column on the right and is not
closed until it has been released. Instead we pull an issue to the next column by changing the label.
The Labels
The following list shows what the labels mean in the life-cycle and will hopefully help you decide how to label an
issue.
Backlog
Why do we have it? To keep us focused on finishing issues that we started, new issues will be hidden in this column.
In huboard you can see the list of things that we could think about by clicking the small arrow in the top left
corner of the concept column header.
What does a developer think? Maybe later.
When can I pull? Since this is the bucket for whatever might be done you should only pick issues from the backlog
when there is no other issue that you can work on. It is more important to finish an issue currently on the Kanban
board than to pull a new one into the flow because only released issues have a value to our users!
Who is Assigned? Either a maintainer feels directly responsible for the issue and assigns himself or the gatekeeper
(the guys having a look at unassigned bugs) will try to determine the responsible developer.
1.43. Bugtracker
167
Concept
Why do we have it? Our think before you act phase serves two purposes. A Bug is in the concept phase while we are
trying to figure out why something is broken (analysis). An Enhancement is in the concept phase until we have
decided how to implement it (design).
What does a developer think? Ill write a Scenario for our BDD in Gherkin and post it as a comment. I can always
look at the existing ones to get an inspiration how to phrase them as Given . . . when . . . then . . .
When can I pull? As long as you think and discuss on how to implement an enhancement or how to solve a bug you
should leave the concept label assigned. Two things should be documented in a comment to the issue before
moving it to the To develop step:
At least one Scenario written in Gherkin that tells you and the tester when the issue is ready to be
released.
A concept describing the planned implementation. This can be as simple as a this just needs changes to
the login screen css or so complex that you link to a blog entry somewhere else.
Who is Assigned? The maintainer that feels responsible for the issue.
To Develop
Why do we have it? Now that we have a plan, any developer can pick an issue from this column and start implementing it. If the issue is also marked with Junior Job this might be a good starting point for new developers.
What does a developer think? Nice! I can safely implement it that way because more than one person has put his
brain to the task of coming up with a good solution. Here! Me! Ill do it!
When can I pull? If you feel like diving into the code and getting your hands dirty you should look for issues with
this label. In the comments, there should be a gherkin scenario to tell you when you are done and a concept
describing how to implement it. Before you start move the issue to the Developing step by assigning the 4
Developing label.
Who is Assigned? No one. Especially not if you are working on something else!
Developing
Why do we have it? This is where the magic happens. If its a Bug the fix will be submitted as a PULL REQUEST
to the master or corresponding stable branch. If its an Enhancement code will be committed to a feature branch.
What does a developer think? You know, Im at it. By the way, Ill also write unit tests. When Im done Ill push
the issue with a commit containing push GH-# where # is the issue number. If I have an idea of who should
review it I can also notify them with @githubusername
When can I pull? As long as you are writing code for the issue or if any unit test fails you should leave the 4
Developing label assigned. Two things should have been implemented before moving the issue to the To
review step:
The enhancement or bug in question
Unit tests for the changed and added code.
Who is Assigned? The most active developer should assign himself.
168
To Review
Why do we have it? Instead of directly committing to master we agree that a second set of eyes will spot bugs
and increase our code quality and give us an opportunity to learn from each other. See also our Code Review
Documentation
What does a developer think? Ill check the Scenario described earlier works as expected. If necessary Ill update
the related Gherkin Scenarios. Jenkins will test the scenario on all kinds of platforms, web server and database
combinations with cucumber.
When can I pull? If you feel like making sure an issue works as expected you should look for issues with this label.
In the comments you should find a gherkin scenario that can be used as a checklist for what to try. Before you
start move the issue to the Reviewing step by assigning the 6 Reviewing label.
Who is Assigned? No one. Especially not if you are working on something else!
Reviewing
Why do we have it? With the Gherkin Scenario from the Concept Phase reviewers have a checklist to test if a Bug
has been solved and if an Enhancement works as expected. The most eager reviewer we have is Jenkins.
When it comes to testing he soldiers on going through the different combinations of platform, web server and
database.
What does a developer think? Damn! If I had written the Gherkin Scenarios and Cucumber Step Definitions I
could leave the task of testing this on the different combinations of platform, web server and database to Jenkins.
Ill miss something when doing this manually.*
When can I pull? As long as you are reviewing the issue the you should leave the 6 Reviewing label assigned.
Before moving the issue to the To review step the issue should have been resolved, meaning that not only the
issue has been implemented but also no other functionality has been broken.
Who is Assigned? The most active reviewer should assign himself.
To Release
Why do we have it? This is a list of issues that will make it into the next release. It serves as a source for the
changelog, as well as a reminder of the work we can already be proud of.
What does a developer think? Look at all the shiny things we will release with the next version of ownCloud!
When can I pull? This is the last step of the Kanban board. When the Release finally happens the issue will be closed
and removed from the board.
Who is Assigned? No one.
While we stated before that said that we push issues to the next column, we can of course move the item back and
forth arbitrarily. Basically you can drag the issue around in the huboard or just change the label when viewing the
issue in the GitHub.
Reviewing considered impossible?
How can you possibly review an issue when it requires you to test various combinations of browsers, platforms,
databases and maybe even app combinations? Well, you cant. But you can write a gherkin scenario that can be used
to write an automated test that is executed by Jenkins on every commit to the main repositories. If for some reason
Jenkins cannot be used for the review you will find yourself in the very uncomfortable situation where you release half
tested code that will hopefully not eat user data. Seriously! Write gherkin scenarios!
1.43. Bugtracker
169
Other Labels
Priority Labels
Panic should be used with caution. It is reserved for Bugs that would result in the loss of files or other user data.
An Enhancement marked as Panic is expected by ownCloud users for the next release. In either case an open
Panic issue will prevent a release.
Attention is not as hard as Panic. But we really want this in the next release and will dedicate more effort for it.
But if we think the issue is not ready for the next release we will postpone it to the next one.
Regression is something that worked in a previous release but is now not working as expected or missing. If a
certain functionality is up for code refactoring, the developer should describe all possible use cases as a Gherkin
scenarios beforehand, so that any scenarios that isnt implemented before the required milestone can be marked
as a regression. If a regression is found after a release, the reporter or the developer triaging the issue should
describe the functionality as a Gherkin scenario and either fix it or assign it to the developer in charge of that
part.
App Labels
In the apps repository there are labels like app:gallery and app:calendar. The app: prefix is used to allow
developers to filter issues related to a specific app.
Resolution Status
Needs info Either from a developer or the bug reporter. This is nearly as severe as Panic, because no further
action can be taken
L18n A translation issue go see our transifex
Junior Job The issue is considered a good starting point to get involved in ownCloud development
Milestones equal Releases
Releases are planned via milestones which contain all the Enhancements and Bugs that we plan to release when the
Deadline is met. When the Deadline approaches we will push new Enhancement request and less important bugs to
the next milestone. This way a milestone will upon release contain all the issues that make up the changelog for the
release. Furthermore, huboard allows us to filter the Kanban board by Milestone, making it especially easy to focus
on the current Release.
170
problem) and in general making sure the bug is useful for a developer who wants to fix it. If the bug is not useful and
cant be augmented by the original reporter or the triaging contributor, it has to be closed.
Why do you want to join
Helping to bring the number if issues down makes it easier for developers to spend their time productively and bug
triagers thus contribute greatly to ownCloud development! Triaging a bug doesnt take long so the work comes in
small chunks and you dont need many skills, just some patience and sometimes perseverance.
Bug triagers who contribute significantly should ask to be listed as an active contributor on the owncloud.org page!
How do you triage bugs
The process of checking, reproducing and closing invalid issues is called bug triaging. Issues can be divided in one
of three kinds:
1. Bugs or feature requests which come with all needed information to allow a developer to fix or work on them
2. Incomplete or duplicate bug reports or feature requests
3. Irrelevant or wrong bug reports or feature requests
The job of a bug triager is to identify the Ones for developers to look at, help remove, merge or improve any Two to
a One and dismiss Threes in a friendly and emphatic way.
Triaging follows these steps:
Find an issue somebody should look at
Be that somebody and see if the issue content is useful for a developer
Reply and close, ask a question, add information or a label.
Find the next bug-to-be-dealt-with and repeat!
General considerations
You need a github account to contribute to bug triaging.
If you are not familiar with the github issue tracker interface (which is used by ownCloud to handle bug reports),
you may find this guide useful.
You will initially only be able to comment on issues. The ability to close issues or assign labels will be given
liberally to those who have shown to be willing and able to contribute. Just ask on IRC!
Read our bug reporting guidelines so you know what a good report should look like and where things belong.
The issue template asks specifically for some information developers need to solve issues.
It might even be fixed, sometimes! It can also be fruitful to contact the developers on irc. Tell them youre
triaging bugs and share what problem you bumped into. Or just ask on the test-pilots mailing list.
To ensure no two people are working on the same issue, we ask you to simply add a comment like I am triaging
this in the issue you want to work on, and when done, before or after executing the triaging actions, note
similarly that youre done.
To be able to tag and close issues, you need to have access to the repository. For the core and sync
app repositories this also means having signed the contributor agreement. However, this isnt really
needed for triaging as you can comment after youre done triaging and somebody else can execute
those actions.
1.43. Bugtracker
171
172
Finding duplicates
To find duplicates, the search tool in github is your first stop. In this screen you can easily search for a few keywords
from the bug report. If you find other bugs with the same content, decide what the best bug report is (often the oldest
or the one where one or more developers have already started to engage and discuss the problem). That is the master
bug report, you can now close the other one (or comment that it can be closed as duplicate).
If the bug report you were reviewing contains additional information, you can add that information to the master bug
report in a comment. Mention this bug report (using #<bug report number>) so a developer can look up the original,
closed, report and perhaps ask the initial reporter there for additional information.
If you cant find anything, look in closed bug reports. The problem might be solved already and be listed there! Of
course, these other bug reports might be closed as duplicates of the one you are looking at now - if you cant find one
that is solved nor can find any duplicates, you can move on to the next step. If you are unsure, just add a comment:
might be a duplicate of #<bug nr here> will usually suffice.
When the issue is a feature request, you can be helpful in the same way: merge related requests by adding information
of one to the other and closing the first.
Note: Be polite: when you need to request information or feedback be clear and polite, and you will get more
information in less time. Think about how youd like to be treated, were you to report a bug!
Note: You can answer more quickly and friendly using one of these templates.
Note: Often our github issue tracker is a place for discussions about solutions. Be friendly, inclusive and respect
other peoples position.
Not all issues are relevant for ownCloud. Bugs can be due to a specific configuration or unsupported platforms.
Raspberry Pis suffer from SQLite time-outs, nginx has problems Apache doesnt and Microsoft Server with IIS is not
well supported. While external issues are not always a reason to close a report, be sure that they are clear: does the
user use the standard platform? Ask for information if this is missing.
Last but not least, the problem might be due to the user doing something that simply does not work. Your general
ownCloud knowledge might be helpful here - if this is the case, you can often swiftly close the issue with a comment
about what went wrong.
Note: You might have to say no to some requests, for example when a problem has been solved in a new release but
wont become available for the release the reporter is using; or when a solution has been chosen which the reporter is
unhappy about. Be considerate. People feel surprisingly strong about ownCloud, and you should take care to explain
that we dont aim to ignore them; on the contrary. But sometimes, decisions which benefit the majority of users dont
help an individual. The extensibility and open availability of the code of ownCloud is here to relieve the pain of such
decisions.
Now that you know that the bug report is unique, and that is not an external issue, you need to check all the needed
information is there.
Check our bug reporting guidelines and make sure bug reports comply with it! The information asked in the issue
template is needed for developers to solve issues.
1.43. Bugtracker
173
Once you added a request for more information, add a #needinfo tag.
If there has been a request for more information on the report, either by you, a developer or somebody else, but the
original reporter (or somebody else who might have the answer) has not responded for 1 month or longer, you can
close the issue. Be polite and note that whoever can answer the question can re-open the issue!
Reproducing the issue
An important step of bug triaging is trying to reproduce the bugs, this means, using the information the reporters added
to the bug report to force (recreate, reproduce, repeat) the bug in the application.
This is needed in order to differentiate random/race condition bugs of reproducible ones (which may be reproduced by
developers too; and they can fix them).
To reproduce an issue, please refer to our testing documents: ownCloud Test Pilots
If you cant reproduce an issue in a newer version of ownCloud, it is most likely fixed and can be closed. Comment
that you failed to reproduce the problem, and if the reporter can confirm (or doesnt respond for a long time), you can
close the issue. Also, be sure to add what exactly you tested with - the ownCloud Master or a branch (and if so, when),
or did you use a release, and if so - what version?
Finalizing and tagging
Once you are done reproducing an issue, it is time to finish up and make clear to the developers what they can do:
If it is a genuine bug (or you are pretty sure it is) add the Bug tag.
If it is a genuine feature request (or you are pretty sure it is) add the enhancement tag.
If the issue is clearly related to something specific, @mention a maintainer. examples: @schiesbn for encryption, @blizzz for LDAP, @PVince81 for quota stuff... You can find a list of maintainers here.
Now, the developers can pick the issue up. Note that while we wish we would always pick up and solve problems
promptly, not all areas of ownCloud get the same amount of attention and contribution, so this can occasionally take a
long time.
Collaboration
You can just get started with bug triaging. But if you want, you can register on the testpilot mailing list and perhaps
introduce yourself to [email protected]. On this list we announce and discuss testing and bug triaging related
subjects.
You can also join the #owncloud-testing channel on irc.freenode.net (link for IRC clients and link to webchat) to ask
questions but keep in mind that people arent active 24/7 and it can occasionally take a while to get a response. Last
but not least, ownCloud contributor Jan Borchardt has a great guide for developers and triagers about dealing with
issues, including some stock answers and thoughts on how to deal with pull requests.
For further questions or help you can also send a mail to:
X (IRC: Y)
We are looking forward to working with you!
Credit: this document is in debt to the extensive KDE guide to bug triaging.
Thank you for helping ownCloud by reporting bugs. Before submitting an issue, please read Issue submission guidelines first.
If the issue is with the ownCloud server, report it to the Core repository
174
If the issue is with the ownCloud client, report it to the Client repository
If the issue with with an ownCloud app, report it to where that app is developed
If the app is listed in our main github repository report it to the correct sub repository
If the app is listed in the apps repository report it there
Please note that the mailing list should not be used for bug reports, as it is hard to track them there.
1.44.3 Maintainers
Contact a maintainer of a certain app or division
175