As a reminder, here are the two relevant PHP files and the relevant template file:
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_old.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonAddForm_old.class.php"
+```
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_old.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonEditForm_old.class.php"
+```
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_old.tpl %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/personAdd_old.tpl"
+```
Updating the template is easy as the complete form is replace by a single line of code:
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/personAdd_new.tpl %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/personAdd_new.tpl"
+```
`PersonEditForm` also becomes much simpler:
only the edited `Person` object must be read:
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonEditForm_new.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonEditForm_new.class.php"
+```
Most of the work is done in `PersonAddForm`:
-{% highlight php %}
-{% include migration/wsc-31/formBuilder/PersonAddForm_new.class.php %}
-{% endhighlight %}
+```php
+--8<-- "migration/wsc-31/formBuilder/PersonAddForm_new.class.php"
+```
But, as you can see, the number of lines almost decreased by half.
All changes are due to extending `AbstractFormBuilderForm`:
The first file for our package is the `install.sql` file used to create such a database table during package installation:
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-1/install.sql %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-1/install.sql"
+```
### Database Object
In our PHP code, each person will be represented by an object of the following class:
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php"
+```
The important thing here is that `Person` extends `DatabaseObject`.
Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
#### `PersonAction`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php"
+```
This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
#### `PersonEditor`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php"
+```
This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
setting the static `$baseClass` property to the database object class name.
#### `PersonList`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php"
+```
Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
1. a third level menu item for the people list page, and
1. a fourth level menu item for the form to add new people.
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/acpMenu.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/acpMenu.xml"
+```
We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
#### `PersonListPage`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php"
+```
As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
#### `personList.tpl`
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personList.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personList.tpl"
+```
We will go piece by piece through the template code:
#### `PersonAddForm`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php"
+```
The properties here consist of two types:
the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
#### `personAdd.tpl`
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl"
+```
We will now only concentrate on the new parts compared to `personList.tpl`:
#### `PersonEditForm`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php"
+```
In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](package_pip_page.md):
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/page.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/page.xml"
+```
For more information about what each of the elements means, please refer to the [page package installation plugin page](package_pip_page.md).
Next, we register the menu item using the [menuItem package installation plugin](package_pip_menu-item.md):
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/menuItem.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/menuItem.xml"
+```
Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
#### `PersonListPage`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php"
+```
This class is almost identical to the ACP version.
In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
#### `personList.tpl`
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-1/templates/personList.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/templates/personList.tpl"
+```
If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](package_pip_user-group-option.md):
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/userGroupOption.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/userGroupOption.xml"
+```
We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
Lastly, we need to create the `package.xml` file.
For more information about this kind of file, please refer to [the `package.xml` page](package_package-xml.md).
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-1/package.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-1/package.xml"
+```
As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](package_pip_sql.md):
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/install.sql %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/install.sql"
+```
If we have a [Person object](tutorial_tutorial-series_part-1-base-structure.md#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
This can be achieved using the `dataFields` template event at whose position we inject the following template code:
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl"
+```
which we store in a `__personAddBirthday.tpl` template file.
The used language item `wcf.person.birthday` is actually the only new one for this package:
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/de.xml %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/de.xml"
+```
-{% highlight sql %}
-{% include tutorial/tutorial-series/part-2/language/en.xml %}
-{% endhighlight %}
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/en.xml"
+```
The template listener needs to be registered using the [templateListener package installation plugin](package_pip_template-listener.md).
The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
The following event listeners achieves these requirements:
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
+```
Some notes on the code:
The first part is a very simple class:
-{% highlight php %}
-{% include tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
+```
!!! info "We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday."
To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl"
+```
!!! info "You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use."
To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
-{% highlight smarty %}
-{% include tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl %}
-{% endhighlight %}
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl"
+```
## `templateListener.xml`
The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/templateListener.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/templateListener.xml"
+```
In cases where a template is used, we simply use the `include` syntax to load the template.
The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/eventListener.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/eventListener.xml"
+```
## `package.xml`
The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-2/package.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-2/package.xml"
+```
---
To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](php_api_caches_runtime-caches.md) for people:
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
+```
## Comments
To allow users to comment on people, we need to tell the system that people support comments.
This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/objectType.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-3/objectType.xml"
+```
The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php"
+```
- First, the system is told the names of the permissions via the `$permission*` properties.
More information about comment permissions can be found [here](php_api_comments.md#user-group-options).
### `PersonPage`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php"
+```
The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
The rest of the code only handles fetching the list of comments on the requested person.
### `person.tpl`
-{% highlight tpl %}
-{% include tutorial/tutorial-series/part-3/templates/person.tpl %}
-{% endhighlight %}
+```tpl
+--8<-- "tutorial/tutorial-series/part-3/templates/person.tpl"
+```
For now, the `person` template is still very empty and only shows the comments in the content area.
The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
### `page.xml`
-{% highlight xml %}
-{% include tutorial/tutorial-series/part-3/page.xml %}
-{% endhighlight %}
+```xml
+--8<-- "tutorial/tutorial-series/part-3/page.xml"
+```
The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
### `PersonPageHandler`
-{% highlight php %}
-{% include tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php %}
-{% endhighlight %}
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php"
+```
Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
As we want administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
- abbr
- pymdownx.highlight
- pymdownx.superfences
+ - pymdownx.snippets:
+ base_path: "snippets/"
extra_css:
- stylesheets/extra.css
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\form\builder\container\FormContainer;
+use wcf\system\form\builder\field\TextFormField;
+
+/**
+ * Shows the form to create a new person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractFormBuilderForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+
+ /**
+ * @inheritDoc
+ */
+ public $formAction = 'create';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public $objectActionClass = PersonAction::class;
+
+ /**
+ * @inheritDoc
+ */
+ protected function createForm() {
+ parent::createForm();
+
+ $dataContainer = FormContainer::create('data')
+ ->appendChildren([
+ TextFormField::create('firstName')
+ ->label('wcf.person.firstName')
+ ->required()
+ ->maximumLength(255),
+
+ TextFormField::create('lastName')
+ ->label('wcf.person.lastName')
+ ->required()
+ ->maximumLength(255)
+ ]);
+
+ $this->form->appendChild($dataContainer);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+
+ /**
+ * first name of the person
+ * @var string
+ */
+ public $firstName = '';
+
+ /**
+ * last name of the person
+ * @var string
+ */
+ public $lastName = '';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'add',
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+ if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ $this->objectAction = new PersonAction([], 'create', [
+ 'data' => array_merge($this->additionalFields, [
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // reset values
+ $this->firstName = '';
+ $this->lastName = '';
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // validate first name
+ if (empty($this->firstName)) {
+ throw new UserInputException('firstName');
+ }
+ if (mb_strlen($this->firstName) > 255) {
+ throw new UserInputException('firstName', 'tooLong');
+ }
+
+ // validate last name
+ if (empty($this->lastName)) {
+ throw new UserInputException('lastName');
+ }
+ if (mb_strlen($this->lastName) > 255) {
+ throw new UserInputException('lastName', 'tooLong');
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\system\exception\IllegalLinkException;
+
+/**
+ * Shows the form to edit an existing person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person';
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) {
+ $this->formObject = new Person(intval($_REQUEST['id']));
+ if (!$this->formObject->personID) {
+ throw new IllegalLinkException();
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person';
+
+ /**
+ * edited person object
+ * @var Person
+ */
+ public $person = null;
+
+ /**
+ * id of the edited person
+ * @var integer
+ */
+ public $personID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'edit',
+ 'person' => $this->person
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->firstName = $this->person->firstName;
+ $this->lastName = $this->person->lastName;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+ $this->person = new Person($this->personID);
+ if (!$this->person->personID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ $this->objectAction = new PersonAction([$this->person], 'update', [
+ 'data' => array_merge($this->additionalFields, [
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{@$form->getHtml()}
+
+{include file='footer'}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+ <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+ <div class="section">
+ <dl{if $errorField == 'firstName'} class="formError"{/if}>
+ <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+ {if $errorField == 'firstName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'lastName'} class="formError"{/if}>
+ <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+ {if $errorField == 'lastName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {event name='dataFields'}
+ </div>
+
+ {event name='sections'}
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ {@SECURITY_TOKEN_INPUT_TAG}
+ </div>
+</form>
+
+{include file='footer'}
--- /dev/null
+<?php
+// include config
+/** @noinspection PhpIncludeInspection */
+require_once(dirname(__FILE__).'/config.inc.php');
+
+// include wcf
+/** @noinspection PhpIncludeInspection */
+require_once(RELATIVE_WCF_DIR.'global.php');
--- /dev/null
+<?php
+require_once('./global.php');
+wcf\system\request\RequestHandler::getInstance()->handle('app');
--- /dev/null
+<?php
+namespace app\page;
+use wcf\page\AbstractPage;
+
+class ExamplePage extends AbstractPage {}
--- /dev/null
+<?php
+namespace app\system;
+use app\page\ExamplePage;
+use wcf\system\application\AbstractApplication;
+
+class APPCore extends AbstractApplication {
+ /**
+ * @inheritDoc
+ */
+ protected $primaryController = ExamplePage::class;
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+ <import>
+ <item identifier="com.example.app.Example">
+ <menu>com.woltlab.wcf.MainMenu</menu>
+ <title language="de">Beispiel-Seite</title>
+ <title language="en">Example Page</title>
+ <page>com.example.app.Example</page>
+ </item>
+ </import>
+</data>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.example.app" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+ <packageinformation>
+ <packagename>Example App</packagename>
+ <packagedescription>A very basic example of an app.</packagedescription>
+ <isapplication>1</isapplication>
+ <version>3.1.0</version>
+ <date>2018-03-29</date>
+ </packageinformation>
+
+ <authorinformation>
+ <author>Example Author</author>
+ <authorurl>https://www.example.com</authorurl>
+ </authorinformation>
+
+ <requiredpackages>
+ <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+ </requiredpackages>
+
+ <compatibility>
+ <api version="2018" />
+ </compatibility>
+
+ <instructions type="install">
+ <instruction type="file" />
+ <instruction type="template" />
+
+ <instruction type="page" />
+ <instruction type="menuItem" />
+ </instructions>
+</package>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+ <import>
+ <page identifier="com.example.app.Example">
+ <pageType>system</pageType>
+ <controller>app\page\ExamplePage</controller>
+ <name language="de">Beispiel-Seite</name>
+ <name language="en">Example App</name>
+ <allowSpidersToIndex>1</allowSpidersToIndex>
+
+ <content language="en">
+ <title>Hello World</title>
+ </content>
+ <content language="de">
+ <title>Hello World</title>
+ </content>
+ </page>
+ </import>
+</data>
--- /dev/null
+{include file='header'}
+
+<div class="section">
+ <p>Example Text</p>
+</div>
+
+{include file='footer'}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/acpMenu.xsd">
+ <import>
+ <acpmenuitem name="wcf.acp.menu.link.person">
+ <parent>wcf.acp.menu.link.content</parent>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.person.list">
+ <controller>wcf\acp\page\PersonListPage</controller>
+ <parent>wcf.acp.menu.link.person</parent>
+ <permissions>admin.content.canManagePeople</permissions>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.person.add">
+ <controller>wcf\acp\form\PersonAddForm</controller>
+ <parent>wcf.acp.menu.link.person.list</parent>
+ <permissions>admin.content.canManagePeople</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+ </import>
+</data>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formNotice'}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+ <div class="section">
+ <dl{if $errorField == 'firstName'} class="formError"{/if}>
+ <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+ {if $errorField == 'firstName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'lastName'} class="formError"{/if}>
+ <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+ {if $errorField == 'lastName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ {event name='dataFields'}
+ </div>
+
+ {event name='sections'}
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ {csrfToken}
+ </div>
+</form>
+
+{include file='footer'}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.list'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.list{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{hascontent}
+ <div class="paginationTop">
+ {content}{pages print=true assign=pagesLinks controller="PersonList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section tabularBox" id="personTableContainer">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnPersonID{if $sortField == 'personID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=personID&sortOrder={if $sortField == 'personID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnTitle columnFirstName{if $sortField == 'firstName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=firstName&sortOrder={if $sortField == 'firstName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.firstName{/lang}</a></th>
+ <th class="columnTitle columnLastName{if $sortField == 'lastName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=lastName&sortOrder={if $sortField == 'lastName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.lastName{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item=person}
+ <tr class="jsPersonRow">
+ <td class="columnIcon">
+ <a href="{link controller='PersonEdit' object=$person}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+ <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$person->personID}" data-confirm-message-html="{lang __encode=true}wcf.acp.person.delete.confirmMessage{/lang}"></span>
+
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID">{#$person->personID}</td>
+ <td class="columnTitle columnFirstName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->firstName}</a></td>
+ <td class="columnTitle columnLastName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->lastName}</a></td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ <nav class="contentFooterNavigation">
+ <ul>
+ <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+
+ {event name='contentFooterNavigation'}
+ </ul>
+ </nav>
+ </footer>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<script data-relocate="true">
+ $(function() {
+ new WCF.Action.Delete('wcf\\data\\person\\PersonAction', '.jsPersonRow');
+
+ var options = { };
+ {if $pages > 1}
+ options.refreshPage = true;
+ {if $pages == $pageNo}
+ options.updatePageNumber = -1;
+ {/if}
+ {else}
+ options.emptyMessage = '{lang}wcf.global.noItems{/lang}';
+ {/if}
+
+ new WCF.Table.EmptyTableHandler($('#personTableContainer'), 'jsPersonRow', options);
+ });
+</script>
+
+{include file='footer'}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+
+ /**
+ * first name of the person
+ * @var string
+ */
+ public $firstName = '';
+
+ /**
+ * last name of the person
+ * @var string
+ */
+ public $lastName = '';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'add',
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+ if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ $this->objectAction = new PersonAction([], 'create', [
+ 'data' => array_merge($this->additionalFields, [
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $returnValues = $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // reset values
+ $this->firstName = '';
+ $this->lastName = '';
+
+ // show success message
+ WCF::getTPL()->assign([
+ 'success' => true,
+ 'objectEditLink' => LinkHandler::getInstance()->getControllerLink(PersonEditForm::class, ['id' => $returnValues['returnValues']->personID]),
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // validate first name
+ if (empty($this->firstName)) {
+ throw new UserInputException('firstName');
+ }
+ if (mb_strlen($this->firstName) > 255) {
+ throw new UserInputException('firstName', 'tooLong');
+ }
+
+ // validate last name
+ if (empty($this->lastName)) {
+ throw new UserInputException('lastName');
+ }
+ if (mb_strlen($this->lastName) > 255) {
+ throw new UserInputException('lastName', 'tooLong');
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person';
+
+ /**
+ * edited person object
+ * @var Person
+ */
+ public $person = null;
+
+ /**
+ * id of the edited person
+ * @var integer
+ */
+ public $personID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'edit',
+ 'person' => $this->person
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->firstName = $this->person->firstName;
+ $this->lastName = $this->person->lastName;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+ $this->person = new Person($this->personID);
+ if (!$this->person->personID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ $this->objectAction = new PersonAction([$this->person], 'update', [
+ 'data' => array_merge($this->additionalFields, [
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\data\person\PersonList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ */
+class PersonListPage extends SortablePage {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.list';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = PersonList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObject;
+use wcf\system\request\IRouteController;
+
+/**
+ * Represents a person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @property-read integer $personID unique id of the person
+ * @property-read string $firstName first name of the person
+ * @property-read string $lastName last name of the person
+ */
+class Person extends DatabaseObject implements IRouteController {
+ /**
+ * Returns the first and last name of the person if a person object is treated as a string.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getTitle();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->firstName . ' ' . $this->lastName;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes person-related actions.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method Person create()
+ * @method PersonEditor[] getObjects()
+ * @method PersonEditor getSingleObject()
+ */
+class PersonAction extends AbstractDatabaseObjectAction {
+ /**
+ * @inheritDoc
+ */
+ protected $permissionsDelete = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ protected $requireACP = ['delete'];
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method static Person create(array $parameters = [])
+ * @method Person getDecoratedObject()
+ * @mixin Person
+ */
+class PersonEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Person::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method Person current()
+ * @method Person[] getObjects()
+ * @method Person|null search($objectID)
+ * @property Person[] $objects
+ */
+class PersonList extends DatabaseObjectList {}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\person\PersonList;
+
+/**
+ * Shows the list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Page
+ */
+class PersonListPage extends SortablePage {
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortField = 'lastName';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = PersonList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
--- /dev/null
+DROP TABLE IF EXISTS wcf1_person;
+CREATE TABLE wcf1_person (
+ personID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ firstName VARCHAR(255) NOT NULL,
+ lastName VARCHAR(255) NOT NULL
+);
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+ <category name="wcf.acp.group">
+ <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Kann Personen verwalten]]></item>
+ </category>
+
+ <category name="wcf.acp.menu">
+ <item name="wcf.acp.menu.link.person"><![CDATA[Personen]]></item>
+ <item name="wcf.acp.menu.link.person.add"><![CDATA[Person hinzufügen]]></item>
+ <item name="wcf.acp.menu.link.person.list"><![CDATA[Personen]]></item>
+ </category>
+
+ <category name="wcf.acp.person">
+ <item name="wcf.acp.person.add"><![CDATA[Person hinzufügen]]></item>
+ <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} die Person <span class="confirmationObject">{$person}</span> wirklich löschen?]]></item>
+ <item name="wcf.acp.person.edit"><![CDATA[Person bearbeiten]]></item>
+ <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[Der Vorname darf nicht länger als 255 Zeichen sein.]]></item>
+ <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[Der Nachname darf nicht länger als 255 Zeichen sein.]]></item>
+ <item name="wcf.acp.person.list"><![CDATA[Personen]]></item>
+ </category>
+
+ <category name="wcf.person">
+ <item name="wcf.person.firstName"><![CDATA[Vorname]]></item>
+ <item name="wcf.person.lastName"><![CDATA[Nachname]]></item>
+ <item name="wcf.person.list"><![CDATA[Personen]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+ <category name="wcf.acp.group">
+ <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Can manage people]]></item>
+ </category>
+
+ <category name="wcf.acp.menu">
+ <item name="wcf.acp.menu.link.person"><![CDATA[People]]></item>
+ <item name="wcf.acp.menu.link.person.add"><![CDATA[Add Person]]></item>
+ <item name="wcf.acp.menu.link.person.list"><![CDATA[People]]></item>
+ </category>
+
+ <category name="wcf.acp.person">
+ <item name="wcf.acp.person.add"><![CDATA[Add Person]]></item>
+ <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[Do you really want to delete the person <span class="confirmationObject">{$person}</span>?]]></item>
+ <item name="wcf.acp.person.edit"><![CDATA[Edit Person]]></item>
+ <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[The first name must not be longer than 255 characters.]]></item>
+ <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[The last name must not be longer than 255 characters.]]></item>
+ <item name="wcf.acp.person.list"><![CDATA[People]]></item>
+ </category>
+
+ <category name="wcf.person">
+ <item name="wcf.person.firstName"><![CDATA[First Name]]></item>
+ <item name="wcf.person.lastName"><![CDATA[Last Name]]></item>
+ <item name="wcf.person.list"><![CDATA[People]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+ <import>
+ <item identifier="com.woltlab.wcf.people.PersonList">
+ <menu>com.woltlab.wcf.MainMenu</menu>
+ <title language="de">Personen</title>
+ <title language="en">People</title>
+ <page>com.woltlab.wcf.people.PersonList</page>
+ </item>
+ </import>
+</data>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+ <packageinformation>
+ <packagename>WoltLab Suite Core Tutorial: People</packagename>
+ <packagedescription>Adds a simple management system for people as part of a tutorial to create packages.</packagedescription>
+ <version>3.1.0</version>
+ <date>2018-03-30</date>
+ </packageinformation>
+
+ <authorinformation>
+ <author>WoltLab GmbH</author>
+ <authorurl>http://www.woltlab.com</authorurl>
+ </authorinformation>
+
+ <requiredpackages>
+ <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+ </requiredpackages>
+
+ <excludedpackages>
+ <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+ </excludedpackages>
+
+ <compatibility>
+ <api version="2018" />
+ </compatibility>
+
+ <instructions type="install">
+ <instruction type="acpTemplate" />
+ <instruction type="file" />
+ <instruction type="sql" />
+ <instruction type="template" />
+ <instruction type="language" />
+
+ <instruction type="acpMenu" />
+ <instruction type="page" />
+ <instruction type="menuItem" />
+ <instruction type="userGroupOption" />
+ </instructions>
+</package>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+ <import>
+ <page identifier="com.woltlab.wcf.people.PersonList">
+ <pageType>system</pageType>
+ <controller>wcf\page\PersonListPage</controller>
+ <name language="de">Personen-Liste</name>
+ <name language="en">Person List</name>
+
+ <content language="de">
+ <title>Personen</title>
+ </content>
+ <content language="en">
+ <title>People</title>
+ </content>
+ </page>
+ </import>
+</data>
--- /dev/null
+{capture assign='contentTitle'}{lang}wcf.person.list{/lang} <span class="badge">{#$items}</span>{/capture}
+
+{capture assign='headContent'}
+ {if $pageNo < $pages}
+ <link rel="next" href="{link controller='PersonList'}pageNo={@$pageNo+1}{/link}">
+ {/if}
+ {if $pageNo > 1}
+ <link rel="prev" href="{link controller='PersonList'}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}">
+ {/if}
+ <link rel="canonical" href="{link controller='PersonList'}{if $pageNo > 1}pageNo={@$pageNo}{/if}{/link}">
+{/capture}
+
+{capture assign='sidebarRight'}
+ <section class="box">
+ <form method="post" action="{link controller='PersonList'}{/link}">
+ <h2 class="boxTitle">{lang}wcf.global.sorting{/lang}</h2>
+
+ <div class="boxContent">
+ <dl>
+ <dt></dt>
+ <dd>
+ <select id="sortField" name="sortField">
+ <option value="firstName"{if $sortField == 'firstName'} selected{/if}>{lang}wcf.person.firstName{/lang}</option>
+ <option value="lastName"{if $sortField == 'lastName'} selected{/if}>{lang}wcf.person.lastName{/lang}</option>
+ {event name='sortField'}
+ </select>
+ <select name="sortOrder">
+ <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+ <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+ </select>
+ </dd>
+ </dl>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ </div>
+ </div>
+ </form>
+ </section>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {pages print=true assign=pagesLinks controller='PersonList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+ {/content}
+ </div>
+{/hascontent}
+
+{if $items}
+ <div class="section sectionContainerList">
+ <ol class="containerList personList">
+ {foreach from=$objects item=person}
+ <li>
+ <div class="box48">
+ <span class="icon icon48 fa-user"></span>
+
+ <div class="details personInformation">
+ <div class="containerHeadline">
+ <h3>{$person}</h3>
+ </div>
+
+ {hascontent}
+ <ul class="inlineList commaSeparated">
+ {content}{event name='personData'}{/content}
+ </ul>
+ {/hascontent}
+
+ {hascontent}
+ <dl class="plain inlineDataList small">
+ {content}{event name='personStatistics'}{/content}
+ </dl>
+ {/hascontent}
+ </div>
+ </div>
+ </li>
+ {/foreach}
+ </ol>
+ </div>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{include file='footer'}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/userGroupOption.xsd">
+ <import>
+ <options>
+ <option name="admin.content.canManagePeople">
+ <categoryname>admin.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ </options>
+ </import>
+</data>
--- /dev/null
+<dl{if $errorField == 'birthday'} class="formError"{/if}>
+ <dt><label for="birthday">{lang}wcf.person.birthday{/lang}</label></dt>
+ <dd>
+ <input type="date" id="birthday" name="birthday" value="{$birthday}">
+ {if $errorField == 'birthday'}
+ <small class="innerError">
+ {if $errorType == 'noValidSelection'}
+ {lang}wcf.global.form.error.noValidSelection{/lang}
+ {else}
+ {lang}wcf.acp.person.birthday.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+</dl>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/eventListener.xsd">
+ <import>
+ <!-- admin -->
+ <eventlistener name="birthdaySortFieldAdminPersonList">
+ <environment>admin</environment>
+ <eventclassname>wcf\acp\page\PersonListPage</eventclassname>
+ <eventname>validateSortField</eventname>
+ <listenerclassname>wcf\system\event\listener\BirthdaySortFieldPersonListPageListener</listenerclassname>
+ </eventlistener>
+ <eventlistener name="birthdayPersonAddForm">
+ <environment>admin</environment>
+ <eventclassname>wcf\acp\form\PersonAddForm</eventclassname>
+ <eventname>saved</eventname>
+ <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+ </eventlistener>
+ <eventlistener name="birthdayPersonAddFormInherited">
+ <environment>admin</environment>
+ <eventclassname>wcf\acp\form\PersonAddForm</eventclassname>
+ <eventname>assignVariables,readFormParameters,save,validate</eventname>
+ <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+ <inherit>1</inherit>
+ </eventlistener>
+ <eventlistener name="birthdayPersonEditForm">
+ <environment>admin</environment>
+ <eventclassname>wcf\acp\form\PersonEditForm</eventclassname>
+ <eventname>readData</eventname>
+ <listenerclassname>wcf\system\event\listener\BirthdayPersonAddFormListener</listenerclassname>
+ </eventlistener>
+ <!-- /admin -->
+
+ <!-- user -->
+ <eventlistener name="birthdaySortFieldPersonList">
+ <environment>user</environment>
+ <eventclassname>wcf\page\PersonListPage</eventclassname>
+ <eventname>validateSortField</eventname>
+ <listenerclassname>wcf\system\event\listener\BirthdaySortFieldPersonListPageListener</listenerclassname>
+ </eventlistener>
+ <!-- /user -->
+ </import>
+</data>
--- /dev/null
+<?php
+namespace wcf\system\event\listener;
+use wcf\acp\form\PersonAddForm;
+use wcf\acp\form\PersonEditForm;
+use wcf\form\IForm;
+use wcf\page\IPage;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Handles setting the birthday when adding and editing people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Event\Listener
+ */
+class BirthdayPersonAddFormListener extends AbstractEventListener {
+ /**
+ * birthday of the created or edited person
+ * @var string
+ */
+ protected $birthday = '';
+
+ /**
+ * @see IPage::assignVariables()
+ */
+ protected function onAssignVariables() {
+ WCF::getTPL()->assign('birthday', $this->birthday);
+ }
+
+ /**
+ * @see IPage::readData()
+ */
+ protected function onReadData(PersonEditForm $form) {
+ if (empty($_POST)) {
+ $this->birthday = $form->person->birthday;
+
+ if ($this->birthday === '0000-00-00') {
+ $this->birthday = '';
+ }
+ }
+ }
+
+ /**
+ * @see IForm::readFormParameters()
+ */
+ protected function onReadFormParameters() {
+ if (isset($_POST['birthday'])) {
+ $this->birthday = StringUtil::trim($_POST['birthday']);
+ }
+ }
+
+ /**
+ * @see IForm::save()
+ */
+ protected function onSave(PersonAddForm $form) {
+ if ($this->birthday) {
+ $form->additionalFields['birthday'] = $this->birthday;
+ }
+ else {
+ $form->additionalFields['birthday'] = '0000-00-00';
+ }
+ }
+
+ /**
+ * @see IForm::saved()
+ */
+ protected function onSaved() {
+ $this->birthday = '';
+ }
+
+ /**
+ * @see IForm::validate()
+ */
+ protected function onValidate() {
+ if (empty($this->birthday)) {
+ return;
+ }
+
+ if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $this->birthday, $match)) {
+ throw new UserInputException('birthday', 'noValidSelection');
+ }
+
+ if (!checkdate(intval($match[2]), intval($match[3]), intval($match[1]))) {
+ throw new UserInputException('birthday', 'noValidSelection');
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\event\listener;
+use wcf\page\SortablePage;
+
+/**
+ * Makes people's birthday a valid sort field in the ACP and the front end.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Event\Listener
+ */
+class BirthdaySortFieldPersonListPageListener implements IParameterizedEventListener {
+ /**
+ * @inheritDoc
+ */
+ public function execute($eventObj, $className, $eventName, array &$parameters) {
+ /** @var SortablePage $eventObj */
+
+ $eventObj->validSortFields[] = 'birthday';
+ }
+}
--- /dev/null
+ALTER TABLE wcf1_person ADD birthday DATE NOT NULL;
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+ <category name="wcf.person">
+ <item name="wcf.person.birthday"><![CDATA[Geburtstag]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+ <category name="wcf.person">
+ <item name="wcf.person.birthday"><![CDATA[Birthday]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people.birthday" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+ <packageinformation>
+ <packagename>WoltLab Suite Core Tutorial: People (Birthday)</packagename>
+ <packagedescription>Adds a birthday field to the people management system as part of a tutorial to create packages.</packagedescription>
+ <version>3.1.0</version>
+ <date>2018-03-30</date>
+ </packageinformation>
+
+ <authorinformation>
+ <author>WoltLab GmbH</author>
+ <authorurl>http://www.woltlab.com</authorurl>
+ </authorinformation>
+
+ <requiredpackages>
+ <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+ <requiredpackage minversion="3.1.0">com.woltlab.wcf.people</requiredpackage>
+ </requiredpackages>
+
+ <excludedpackages>
+ <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+ </excludedpackages>
+
+ <compatibility>
+ <api version="2018" />
+ </compatibility>
+
+ <instructions type="install">
+ <instruction type="acpTemplate" />
+ <instruction type="file" />
+ <instruction type="sql" />
+ <instruction type="template" />
+ <instruction type="language" />
+
+ <instruction type="eventListener" />
+ <instruction type="templateListener" />
+ </instructions>
+</package>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/tornado/XSD/templateListener.xsd">
+ <import>
+ <!-- admin -->
+ <templatelistener name="personListBirthdayColumnHead">
+ <eventname>columnHeads</eventname>
+ <environment>admin</environment>
+ <templatecode><![CDATA[<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>]]></templatecode>
+ <templatename>personList</templatename>
+ </templatelistener>
+ <templatelistener name="personListBirthdayColumn">
+ <eventname>columns</eventname>
+ <environment>admin</environment>
+ <templatecode><![CDATA[<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>]]></templatecode>
+ <templatename>personList</templatename>
+ </templatelistener>
+ <templatelistener name="personAddBirthday">
+ <eventname>dataFields</eventname>
+ <environment>admin</environment>
+ <templatecode><![CDATA[{include file='__personAddBirthday'}]]></templatecode>
+ <templatename>personAdd</templatename>
+ </templatelistener>
+ <!-- /admin -->
+
+ <!-- user -->
+ <templatelistener name="personListBirthday">
+ <eventname>personStatistics</eventname>
+ <environment>user</environment>
+ <templatecode><![CDATA[{include file='__personListBirthday'}]]></templatecode>
+ <templatename>personList</templatename>
+ </templatelistener>
+ <templatelistener name="personListBirthdaySortField">
+ <eventname>sortField</eventname>
+ <environment>user</environment>
+ <templatecode><![CDATA[{include file='__personListBirthdaySortField'}]]></templatecode>
+ <templatename>personList</templatename>
+ </templatelistener>
+ <!-- /user -->
+ </import>
+</data>
--- /dev/null
+{if $person->birthday !== '0000-00-00'}
+ <dt>{lang}wcf.person.birthday{/lang}</dt>
+ <dd>{@$person->birthday|strtotime|date}</dd>
+{/if}
--- /dev/null
+<option value="birthday"{if $sortField == 'birthday'} selected{/if}>{lang}wcf.person.birthday{/lang}</option>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/acpMenu.xsd">
+ <import>
+ <acpmenuitem name="wcf.acp.menu.link.person">
+ <parent>wcf.acp.menu.link.content</parent>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.person.list">
+ <controller>wcf\acp\page\PersonListPage</controller>
+ <parent>wcf.acp.menu.link.person</parent>
+ <permissions>admin.content.canManagePeople</permissions>
+ </acpmenuitem>
+ <acpmenuitem name="wcf.acp.menu.link.person.add">
+ <controller>wcf\acp\form\PersonAddForm</controller>
+ <parent>wcf.acp.menu.link.person.list</parent>
+ <permissions>admin.content.canManagePeople</permissions>
+ <icon>fa-plus</icon>
+ </acpmenuitem>
+ </import>
+</data>
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.'|concat:$action}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.{$action}{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonList'}{/link}" class="button"><span class="icon icon16 fa-list"></span> <span>{lang}wcf.acp.menu.link.person.list{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{include file='formNotice'}
+
+<form method="post" action="{if $action == 'add'}{link controller='PersonAdd'}{/link}{else}{link controller='PersonEdit' object=$person}{/link}{/if}">
+ <div class="section">
+ <dl{if $errorField == 'firstName'} class="formError"{/if}>
+ <dt><label for="firstName">{lang}wcf.person.firstName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="firstName" name="firstName" value="{$firstName}" required autofocus maxlength="255" class="long">
+ {if $errorField == 'firstName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.firstName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl{if $errorField == 'lastName'} class="formError"{/if}>
+ <dt><label for="lastName">{lang}wcf.person.lastName{/lang}</label></dt>
+ <dd>
+ <input type="text" id="lastName" name="lastName" value="{$lastName}" required maxlength="255" class="long">
+ {if $errorField == 'lastName'}
+ <small class="innerError">
+ {if $errorType == 'empty'}
+ {lang}wcf.global.form.error.empty{/lang}
+ {else}
+ {lang}wcf.acp.person.lastName.error.{$errorType}{/lang}
+ {/if}
+ </small>
+ {/if}
+ </dd>
+ </dl>
+
+ <dl>
+ <dt></dt>
+ <dd>
+ <label><input name="enableComments" type="checkbox" value="1"{if $enableComments} checked{/if}> {lang}wcf.person.enableComments{/lang}</label>
+ <small>{lang}wcf.person.enableComments.description{/lang}</small>
+ </dd>
+ </dl>
+
+ {event name='dataFields'}
+ </div>
+
+ {event name='sections'}
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ {csrfToken}
+ </div>
+</form>
+
+{include file='footer'}
--- /dev/null
+{include file='header' pageTitle='wcf.acp.person.list'}
+
+<header class="contentHeader">
+ <div class="contentHeaderTitle">
+ <h1 class="contentTitle">{lang}wcf.acp.person.list{/lang}</h1>
+ </div>
+
+ <nav class="contentHeaderNavigation">
+ <ul>
+ <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+
+ {event name='contentHeaderNavigation'}
+ </ul>
+ </nav>
+</header>
+
+{hascontent}
+ <div class="paginationTop">
+ {content}{pages print=true assign=pagesLinks controller="PersonList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content}
+ </div>
+{/hascontent}
+
+{if $objects|count}
+ <div class="section tabularBox" id="personTableContainer">
+ <table class="table">
+ <thead>
+ <tr>
+ <th class="columnID columnPersonID{if $sortField == 'personID'} active {@$sortOrder}{/if}" colspan="2"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=personID&sortOrder={if $sortField == 'personID' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.global.objectID{/lang}</a></th>
+ <th class="columnTitle columnFirstName{if $sortField == 'firstName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=firstName&sortOrder={if $sortField == 'firstName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.firstName{/lang}</a></th>
+ <th class="columnTitle columnLastName{if $sortField == 'lastName'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=lastName&sortOrder={if $sortField == 'lastName' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.lastName{/lang}</a></th>
+
+ {event name='columnHeads'}
+ </tr>
+ </thead>
+
+ <tbody>
+ {foreach from=$objects item=person}
+ <tr class="jsPersonRow">
+ <td class="columnIcon">
+ <a href="{link controller='PersonEdit' object=$person}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 fa-pencil"></span></a>
+ <span class="icon icon16 fa-times jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$person->personID}" data-confirm-message-html="{lang __encode=true}wcf.acp.person.delete.confirmMessage{/lang}"></span>
+
+ {event name='rowButtons'}
+ </td>
+ <td class="columnID">{#$person->personID}</td>
+ <td class="columnTitle columnFirstName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->firstName}</a></td>
+ <td class="columnTitle columnLastName"><a href="{link controller='PersonEdit' object=$person}{/link}">{$person->lastName}</a></td>
+
+ {event name='columns'}
+ </tr>
+ {/foreach}
+ </tbody>
+ </table>
+ </div>
+
+ <footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ <nav class="contentFooterNavigation">
+ <ul>
+ <li><a href="{link controller='PersonAdd'}{/link}" class="button"><span class="icon icon16 fa-plus"></span> <span>{lang}wcf.acp.menu.link.person.add{/lang}</span></a></li>
+
+ {event name='contentFooterNavigation'}
+ </ul>
+ </nav>
+ </footer>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<script data-relocate="true">
+ $(function() {
+ new WCF.Action.Delete('wcf\\data\\person\\PersonAction', '.jsPersonRow');
+
+ var options = { };
+ {if $pages > 1}
+ options.refreshPage = true;
+ {if $pages == $pageNo}
+ options.updatePageNumber = -1;
+ {/if}
+ {else}
+ options.emptyMessage = '{lang}wcf.global.noItems{/lang}';
+ {/if}
+
+ new WCF.Table.EmptyTableHandler($('#personTableContainer'), 'jsPersonRow', options);
+ });
+</script>
+
+{include file='footer'}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonAddForm extends AbstractForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.add';
+
+ /**
+ * is `1` if comments are enabled for the person, otherwise `0`
+ * @var integer
+ */
+ public $enableComments = 1;
+
+ /**
+ * first name of the person
+ * @var string
+ */
+ public $firstName = '';
+
+ /**
+ * last name of the person
+ * @var string
+ */
+ public $lastName = '';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'add',
+ 'enableComments' => $this->enableComments,
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readFormParameters() {
+ parent::readFormParameters();
+
+ $this->enableComments = isset($_POST['enableComments']) ? 1 : 0;
+ if (isset($_POST['firstName'])) $this->firstName = StringUtil::trim($_POST['firstName']);
+ if (isset($_POST['lastName'])) $this->lastName = StringUtil::trim($_POST['lastName']);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ $this->objectAction = new PersonAction([], 'create', [
+ 'data' => array_merge($this->additionalFields, [
+ 'enableComments' => $this->enableComments,
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $returnValues = $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // reset values
+ $this->enableComments = 1;
+ $this->firstName = '';
+ $this->lastName = '';
+
+ // show success message
+ WCF::getTPL()->assign([
+ 'success' => true,
+ 'objectEditLink' => LinkHandler::getInstance()->getControllerLink(PersonEditForm::class, ['id' => $returnValues['returnValues']->personID]),
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate() {
+ parent::validate();
+
+ // validate first name
+ if (empty($this->firstName)) {
+ throw new UserInputException('firstName');
+ }
+ if (mb_strlen($this->firstName) > 255) {
+ throw new UserInputException('firstName', 'tooLong');
+ }
+
+ // validate last name
+ if (empty($this->lastName)) {
+ throw new UserInputException('lastName');
+ }
+ if (mb_strlen($this->lastName) > 255) {
+ throw new UserInputException('lastName', 'tooLong');
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\person\Person;
+use wcf\data\person\PersonAction;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ */
+class PersonEditForm extends PersonAddForm {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person';
+
+ /**
+ * edited person object
+ * @var Person
+ */
+ public $person = null;
+
+ /**
+ * id of the edited person
+ * @var integer
+ */
+ public $personID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'action' => 'edit',
+ 'person' => $this->person
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if (empty($_POST)) {
+ $this->enableComments = $this->person->enableComments;
+ $this->firstName = $this->person->firstName;
+ $this->lastName = $this->person->lastName;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+ $this->person = new Person($this->personID);
+ if (!$this->person->personID) {
+ throw new IllegalLinkException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ $this->objectAction = new PersonAction([$this->person], 'update', [
+ 'data' => array_merge($this->additionalFields, [
+ 'enableComments' => $this->enableComments,
+ 'firstName' => $this->firstName,
+ 'lastName' => $this->lastName
+ ])
+ ]);
+ $this->objectAction->executeAction();
+
+ $this->saved();
+
+ // show success message
+ WCF::getTPL()->assign('success', true);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\page;
+use wcf\data\person\PersonList;
+use wcf\page\SortablePage;
+
+/**
+ * Shows the list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Page
+ */
+class PersonListPage extends SortablePage {
+ /**
+ * @inheritDoc
+ */
+ public $activeMenuItem = 'wcf.acp.menu.link.person.list';
+
+ /**
+ * @inheritDoc
+ */
+ public $neededPermissions = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = PersonList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObject;
+use wcf\data\ILinkableObject;
+use wcf\system\request\IRouteController;
+use wcf\system\request\LinkHandler;
+
+/**
+ * Represents a person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @property-read integer $personID unique id of the person
+ * @property-read string $firstName first name of the person
+ * @property-read string $lastName last name of the person
+ * @property-read integer $comments number of comments on the person
+ * @property-read integer $enableComments is `1` if comments are enabled for the person, otherwise `0`
+ */
+class Person extends DatabaseObject implements ILinkableObject, IRouteController {
+ /**
+ * Returns the first and last name of the person if a person object is treated as a string.
+ *
+ * @return string
+ */
+ public function __toString() {
+ return $this->getTitle();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink() {
+ return LinkHandler::getInstance()->getLink('Person', [
+ 'forceFrontend' => true,
+ 'object' => $this
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle() {
+ return $this->firstName . ' ' . $this->lastName;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\AbstractDatabaseObjectAction;
+
+/**
+ * Executes person-related actions.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method Person create()
+ * @method PersonEditor[] getObjects()
+ * @method PersonEditor getSingleObject()
+ */
+class PersonAction extends AbstractDatabaseObjectAction {
+ /**
+ * @inheritDoc
+ */
+ protected $permissionsDelete = ['admin.content.canManagePeople'];
+
+ /**
+ * @inheritDoc
+ */
+ protected $requireACP = ['delete'];
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectEditor;
+
+/**
+ * Provides functions to edit people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method static Person create(array $parameters = [])
+ * @method Person getDecoratedObject()
+ * @mixin Person
+ */
+class PersonEditor extends DatabaseObjectEditor {
+ /**
+ * @inheritDoc
+ */
+ protected static $baseClass = Person::class;
+}
--- /dev/null
+<?php
+namespace wcf\data\person;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Data\Person
+ *
+ * @method Person current()
+ * @method Person[] getObjects()
+ * @method Person|null search($objectID)
+ * @property Person[] $objects
+ */
+class PersonList extends DatabaseObjectList {}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\person\PersonList;
+
+/**
+ * Shows the list of people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Page
+ */
+class PersonListPage extends SortablePage {
+ /**
+ * @inheritDoc
+ */
+ public $defaultSortField = 'lastName';
+
+ /**
+ * @inheritDoc
+ */
+ public $objectListClassName = PersonList::class;
+
+ /**
+ * @inheritDoc
+ */
+ public $validSortFields = ['personID', 'firstName', 'lastName'];
+}
--- /dev/null
+<?php
+namespace wcf\page;
+use wcf\data\person\Person;
+use wcf\system\comment\CommentHandler;
+use wcf\system\comment\manager\PersonCommentManager;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\WCF;
+
+/**
+ * Shows the details of a certain person.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Page
+ */
+class PersonPage extends AbstractPage {
+ /**
+ * list of comments
+ * @var StructuredCommentList
+ */
+ public $commentList;
+
+ /**
+ * person comment manager object
+ * @var PersonCommentManager
+ */
+ public $commentManager;
+
+ /**
+ * id of the person comment object type
+ * @var integer
+ */
+ public $commentObjectTypeID = 0;
+
+ /**
+ * shown person
+ * @var Person
+ */
+ public $person;
+
+ /**
+ * id of the shown person
+ * @var integer
+ */
+ public $personID = 0;
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'commentCanAdd' => WCF::getSession()->getPermission('user.person.canAddComment'),
+ 'commentList' => $this->commentList,
+ 'commentObjectTypeID' => $this->commentObjectTypeID,
+ 'lastCommentTime' => $this->commentList ? $this->commentList->getMinCommentTime() : 0,
+ 'likeData' => (MODULE_LIKE && $this->commentList) ? $this->commentList->getLikeData() : [],
+ 'person' => $this->person
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readData() {
+ parent::readData();
+
+ if ($this->person->enableComments) {
+ $this->commentObjectTypeID = CommentHandler::getInstance()->getObjectTypeID('com.woltlab.wcf.person.personComment');
+ $this->commentManager = CommentHandler::getInstance()->getObjectType($this->commentObjectTypeID)->getProcessor();
+ $this->commentList = CommentHandler::getInstance()->getCommentList($this->commentManager, $this->commentObjectTypeID, $this->person->personID);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (isset($_REQUEST['id'])) $this->personID = intval($_REQUEST['id']);
+ $this->person = new Person($this->personID);
+ if (!$this->person->personID) {
+ throw new IllegalLinkException();
+ }
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\cache\runtime;
+use wcf\data\person\Person;
+use wcf\data\person\PersonList;
+
+/**
+ * Runtime cache implementation for people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Cache\Runtime
+ * @since 3.0
+ *
+ * @method Person[] getCachedObjects()
+ * @method Person getObject($objectID)
+ * @method Person[] getObjects(array $objectIDs)
+ */
+class PersonRuntimeCache extends AbstractRuntimeCache {
+ /**
+ * @inheritDoc
+ */
+ protected $listClassName = PersonList::class;
+}
--- /dev/null
+<?php
+namespace wcf\system\comment\manager;
+use wcf\data\person\Person;
+use wcf\data\person\PersonEditor;
+use wcf\system\cache\runtime\PersonRuntimeCache;
+use wcf\system\WCF;
+
+/**
+ * Comment manager implementation for people.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Comment\Manager
+ */
+class PersonCommentManager extends AbstractCommentManager {
+ /**
+ * @inheritDoc
+ */
+ protected $permissionAdd = 'user.person.canAddComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionAddWithoutModeration = 'user.person.canAddCommentWithoutModeration';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionCanModerate = 'mod.person.canModerateComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionDelete = 'user.person.canDeleteComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionEdit = 'user.person.canEditComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionModDelete = 'mod.person.canDeleteComment';
+
+ /**
+ * @inheritDoc
+ */
+ protected $permissionModEdit = 'mod.person.canEditComment';
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink($objectTypeID, $objectID) {
+ return PersonRuntimeCache::getInstance()->getObject($objectID)->getLink();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isAccessible($objectID, $validateWritePermission = false) {
+ return PersonRuntimeCache::getInstance()->getObject($objectID) !== null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle($objectTypeID, $objectID, $isResponse = false) {
+ if ($isResponse) {
+ return WCF::getLanguage()->get('wcf.person.commentResponse');
+ }
+
+ return WCF::getLanguage()->getDynamicVariable('wcf.person.comment');
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function updateCounter($objectID, $value) {
+ (new PersonEditor(new Person($objectID)))->updateCounters(['comments' => $value]);
+ }
+}
--- /dev/null
+<?php
+namespace wcf\system\page\handler;
+use wcf\data\page\Page;
+use wcf\data\person\PersonList;
+use wcf\data\user\online\UserOnline;
+use wcf\system\cache\runtime\PersonRuntimeCache;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\WCF;
+
+/**
+ * Page handler implementation for person page.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Page\Handler
+ */
+class PersonPageHandler extends AbstractLookupPageHandler implements IOnlineLocationPageHandler {
+ use TOnlineLocationPageHandler;
+
+ /**
+ * @inheritDoc
+ */
+ public function getLink($objectID) {
+ return PersonRuntimeCache::getInstance()->getObject($objectID)->getLink();
+ }
+
+ /**
+ * Returns the textual description if a user is currently online viewing this page.
+ *
+ * @see IOnlineLocationPageHandler::getOnlineLocation()
+ *
+ * @param Page $page visited page
+ * @param UserOnline $user user online object with request data
+ * @return string
+ */
+ public function getOnlineLocation(Page $page, UserOnline $user) {
+ if ($user->pageObjectID === null) {
+ return '';
+ }
+
+ $person = PersonRuntimeCache::getInstance()->getObject($user->pageObjectID);
+ if ($person === null) {
+ return '';
+ }
+
+ return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, ['person' => $person]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function isValid($objectID = null) {
+ return PersonRuntimeCache::getInstance()->getObject($objectID) !== null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function lookup($searchString) {
+ $conditionBuilder = new PreparedStatementConditionBuilder(false, 'OR');
+ $conditionBuilder->add('person.firstName LIKE ?', ['%' . $searchString . '%']);
+ $conditionBuilder->add('person.lastName LIKE ?', ['%' . $searchString . '%']);
+
+ $personList = new PersonList();
+ $personList->getConditionBuilder()->add($conditionBuilder, $conditionBuilder->getParameters());
+ $personList->readObjects();
+
+ $results = [];
+ foreach ($personList as $person) {
+ $results[] = [
+ 'image' => 'fa-user',
+ 'link' => $person->getLink(),
+ 'objectID' => $person->personID,
+ 'title' => $person->getTitle()
+ ];
+ }
+
+ return $results;
+ }
+
+ /**
+ * Prepares fetching all necessary data for the textual description if a user is currently online
+ * viewing this page.
+ *
+ * @see IOnlineLocationPageHandler::prepareOnlineLocation()
+ *
+ * @param Page $page visited page
+ * @param UserOnline $user user online object with request data
+ */
+ public function prepareOnlineLocation(/** @noinspection PhpUnusedParameterInspection */Page $page, UserOnline $user) {
+ if ($user->pageObjectID !== null) {
+ PersonRuntimeCache::getInstance()->cacheObjectID($user->pageObjectID);
+ }
+ }
+}
--- /dev/null
+DROP TABLE IF EXISTS wcf1_person;
+CREATE TABLE wcf1_person (
+ personID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ firstName VARCHAR(255) NOT NULL,
+ lastName VARCHAR(255) NOT NULL,
+ comments SMALLINT(5) NOT NULL DEFAULT 0,
+ enableComments TINYINT(1) NOT NULL DEFAULT 1
+);
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="de">
+ <category name="wcf.acp.group">
+ <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Kann Personen verwalten]]></item>
+
+ <item name="wcf.acp.group.option.category.mod.person"><![CDATA[Personen]]></item>
+ <item name="wcf.acp.group.option.category.user.person"><![CDATA[Personen]]></item>
+
+ <item name="wcf.acp.group.option.mod.person.canDeleteComment"><![CDATA[Kann Kommentare löschen]]></item>
+ <item name="wcf.acp.group.option.mod.person.canEditComment"><![CDATA[Kann Kommentare bearbeiten]]></item>
+ <item name="wcf.acp.group.option.mod.person.canModerateComment"><![CDATA[Kann Kommentare moderieren]]></item>
+
+ <item name="wcf.acp.group.option.user.person.canAddComment"><![CDATA[Kann Kommentare erstellen]]></item>
+ <item name="wcf.acp.group.option.user.person.canDeleteComment"><![CDATA[Kann eigene Kommentare löschen]]></item>
+ <item name="wcf.acp.group.option.user.person.canEditComment"><![CDATA[Kann eigene Kommentare bearbeiten]]></item>
+ </category>
+
+ <category name="wcf.acp.menu">
+ <item name="wcf.acp.menu.link.person"><![CDATA[Personen]]></item>
+ <item name="wcf.acp.menu.link.person.add"><![CDATA[Person hinzufügen]]></item>
+ <item name="wcf.acp.menu.link.person.list"><![CDATA[Personen]]></item>
+ </category>
+
+ <category name="wcf.acp.person">
+ <item name="wcf.acp.person.add"><![CDATA[Person hinzufügen]]></item>
+ <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} die Person <span class="confirmationObject">{$person}</span> wirklich löschen?]]></item>
+ <item name="wcf.acp.person.edit"><![CDATA[Person bearbeiten]]></item>
+ <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[Der Vorname darf nicht länger als 255 Zeichen sein.]]></item>
+ <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[Der Nachname darf nicht länger als 255 Zeichen sein.]]></item>
+ <item name="wcf.acp.person.list"><![CDATA[Personen]]></item>
+ </category>
+
+ <category name="wcf.page">
+ <item name="wcf.page.onlineLocation.com.woltlab.wcf.people.Person"><![CDATA[Person <a href="{$person->getLink()}">{$person}</a>]]></item>
+ </category>
+
+ <category name="wcf.person">
+ <item name="wcf.person.comment"><![CDATA[Person-Kommentar]]></item>
+ <item name="wcf.person.commentResponse"><![CDATA[Antwort auf Person-Kommentar]]></item>
+ <item name="wcf.person.comments"><![CDATA[Kommentare]]></item>
+ <item name="wcf.person.enableComments"><![CDATA[Kommentare aktivieren]]></item>
+ <item name="wcf.person.enableComments.description"><![CDATA[Erlaubt es Benutzern diese Person zu kommentieren.]]></item>
+ <item name="wcf.person.firstName"><![CDATA[Vorname]]></item>
+ <item name="wcf.person.lastName"><![CDATA[Nachname]]></item>
+ <item name="wcf.person.list"><![CDATA[Personen]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/language.xsd" languagecode="en">
+ <category name="wcf.acp.group">
+ <item name="wcf.acp.group.option.admin.content.canManagePeople"><![CDATA[Can manage people]]></item>
+
+ <item name="wcf.acp.group.option.category.mod.person"><![CDATA[People]]></item>
+ <item name="wcf.acp.group.option.category.user.person"><![CDATA[People]]></item>
+
+ <item name="wcf.acp.group.option.mod.person.canDeleteComment"><![CDATA[Can delete comments]]></item>
+ <item name="wcf.acp.group.option.mod.person.canEditComment"><![CDATA[Can edit comments]]></item>
+ <item name="wcf.acp.group.option.mod.person.canModerateComment"><![CDATA[Can moderate comments]]></item>
+
+ <item name="wcf.acp.group.option.user.person.canAddComment"><![CDATA[Can create comments]]></item>
+ <item name="wcf.acp.group.option.user.person.canDeleteComment"><![CDATA[Can delete their comments]]></item>
+ <item name="wcf.acp.group.option.user.person.canEditComment"><![CDATA[Can edit their comments]]></item>
+ </category>
+
+ <category name="wcf.acp.menu">
+ <item name="wcf.acp.menu.link.person"><![CDATA[People]]></item>
+ <item name="wcf.acp.menu.link.person.add"><![CDATA[Add Person]]></item>
+ <item name="wcf.acp.menu.link.person.list"><![CDATA[People]]></item>
+ </category>
+
+ <category name="wcf.acp.person">
+ <item name="wcf.acp.person.add"><![CDATA[Add Person]]></item>
+ <item name="wcf.acp.person.delete.confirmMessage"><![CDATA[Do you really want to delete the person <span class="confirmationObject">{$person}</span>?]]></item>
+ <item name="wcf.acp.person.edit"><![CDATA[Edit Person]]></item>
+ <item name="wcf.acp.person.firstName.error.tooLong"><![CDATA[The first name must not be longer than 255 characters.]]></item>
+ <item name="wcf.acp.person.lastName.error.tooLong"><![CDATA[The last name must not be longer than 255 characters.]]></item>
+ <item name="wcf.acp.person.list"><![CDATA[People]]></item>
+ </category>
+
+ <category name="wcf.page">
+ <item name="wcf.page.onlineLocation.com.woltlab.wcf.people.Person"><![CDATA[Person <a href="{$person->getLink()}">{$person}</a>]]></item>
+ </category>
+
+ <category name="wcf.person">
+ <item name="wcf.person.comment"><![CDATA[Person Comment]]></item>
+ <item name="wcf.person.commentResponse"><![CDATA[Reply to Person Comment]]></item>
+ <item name="wcf.person.comments"><![CDATA[Comments]]></item>
+ <item name="wcf.person.enableComments"><![CDATA[Allow Comments]]></item>
+ <item name="wcf.person.enableComments.description"><![CDATA[Allow users to comment on this person.]]></item>
+ <item name="wcf.person.firstName"><![CDATA[First Name]]></item>
+ <item name="wcf.person.lastName"><![CDATA[Last Name]]></item>
+ <item name="wcf.person.list"><![CDATA[People]]></item>
+ </category>
+</language>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/menuItem.xsd">
+ <import>
+ <item identifier="com.woltlab.wcf.people.PersonList">
+ <menu>com.woltlab.wcf.MainMenu</menu>
+ <title language="de">Personen</title>
+ <title language="en">People</title>
+ <page>com.woltlab.wcf.people.PersonList</page>
+ </item>
+ </import>
+</data>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/objectType.xsd">
+ <import>
+ <type>
+ <name>com.woltlab.wcf.person.personComment</name>
+ <definitionname>com.woltlab.wcf.comment.commentableContent</definitionname>
+ <classname>wcf\system\comment\manager\PersonCommentManager</classname>
+ </type>
+ </import>
+</data>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<package name="com.woltlab.wcf.people" xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/package.xsd">
+ <packageinformation>
+ <packagename>WoltLab Suite Core Tutorial: People</packagename>
+ <packagedescription>Adds a simple management system for people as part of a tutorial to create packages.</packagedescription>
+ <version>3.1.0</version>
+ <date>2018-03-30</date>
+ </packageinformation>
+
+ <authorinformation>
+ <author>WoltLab GmbH</author>
+ <authorurl>http://www.woltlab.com</authorurl>
+ </authorinformation>
+
+ <requiredpackages>
+ <requiredpackage minversion="3.1.0">com.woltlab.wcf</requiredpackage>
+ </requiredpackages>
+
+ <excludedpackages>
+ <excludedpackage version="3.2.0 Alpha 1">com.woltlab.wcf</excludedpackage>
+ </excludedpackages>
+
+ <compatibility>
+ <api version="2018" />
+ </compatibility>
+
+ <instructions type="install">
+ <instruction type="acpTemplate" />
+ <instruction type="file" />
+ <instruction type="sql" />
+ <instruction type="template" />
+ <instruction type="language" />
+ <instruction type="objectType" />
+
+ <instruction type="acpMenu" />
+ <instruction type="page" />
+ <instruction type="menuItem" />
+ <instruction type="userGroupOption" />
+ </instructions>
+</package>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/page.xsd">
+ <import>
+ <page identifier="com.woltlab.wcf.people.PersonList">
+ <pageType>system</pageType>
+ <controller>wcf\page\PersonListPage</controller>
+ <name language="de">Personen-Liste</name>
+ <name language="en">Person List</name>
+
+ <content language="de">
+ <title>Personen</title>
+ </content>
+ <content language="en">
+ <title>People</title>
+ </content>
+ </page>
+ <page identifier="com.woltlab.wcf.people.Person">
+ <pageType>system</pageType>
+ <controller>wcf\page\PersonPage</controller>
+ <handler>wcf\system\page\handler\PersonPageHandler</handler>
+ <name language="de">Person</name>
+ <name language="en">Person</name>
+ <requireObjectID>1</requireObjectID>
+ <parent>com.woltlab.wcf.people.PersonList</parent>
+ </page>
+ </import>
+</data>
--- /dev/null
+{capture assign='pageTitle'}{$person} - {lang}wcf.person.list{/lang}{/capture}
+
+{capture assign='contentTitle'}{$person}{/capture}
+
+{include file='header'}
+
+{if $person->enableComments}
+ {if $commentList|count || $commentCanAdd}
+ <section id="comments" class="section sectionContainerList">
+ <header class="sectionHeader">
+ <h2 class="sectionTitle">{lang}wcf.person.comments{/lang}{if $person->comments} <span class="badge">{#$person->comments}</span>{/if}</h2>
+ </header>
+
+ {include file='__commentJavaScript' commentContainerID='personCommentList'}
+
+ <div class="personComments">
+ <ul id="personCommentList" class="commentList containerList"
+ data-can-add="{if $commentCanAdd}true{else}false{/if}"
+ data-object-id="{@$person->personID}"
+ data-object-type-id="{@$commentObjectTypeID}"
+ data-comments="{if $person->comments}{@$commentList->countObjects()}{else}0{/if}"
+ data-last-comment-time="{@$lastCommentTime}"
+ >
+ {include file='commentListAddComment' wysiwygSelector='personCommentListAddComment'}
+ {include file='commentList'}
+ </ul>
+ </div>
+ </section>
+ {/if}
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{include file='footer'}
--- /dev/null
+{capture assign='contentTitle'}{lang}wcf.person.list{/lang} <span class="badge">{#$items}</span>{/capture}
+
+{capture assign='headContent'}
+ {if $pageNo < $pages}
+ <link rel="next" href="{link controller='PersonList'}pageNo={@$pageNo+1}{/link}">
+ {/if}
+ {if $pageNo > 1}
+ <link rel="prev" href="{link controller='PersonList'}{if $pageNo > 2}pageNo={@$pageNo-1}{/if}{/link}">
+ {/if}
+ <link rel="canonical" href="{link controller='PersonList'}{if $pageNo > 1}pageNo={@$pageNo}{/if}{/link}">
+{/capture}
+
+{capture assign='sidebarRight'}
+ <section class="box">
+ <form method="post" action="{link controller='PersonList'}{/link}">
+ <h2 class="boxTitle">{lang}wcf.global.sorting{/lang}</h2>
+
+ <div class="boxContent">
+ <dl>
+ <dt></dt>
+ <dd>
+ <select id="sortField" name="sortField">
+ <option value="firstName"{if $sortField == 'firstName'} selected{/if}>{lang}wcf.person.firstName{/lang}</option>
+ <option value="lastName"{if $sortField == 'lastName'} selected{/if}>{lang}wcf.person.lastName{/lang}</option>
+ {event name='sortField'}
+ </select>
+ <select name="sortOrder">
+ <option value="ASC"{if $sortOrder == 'ASC'} selected{/if}>{lang}wcf.global.sortOrder.ascending{/lang}</option>
+ <option value="DESC"{if $sortOrder == 'DESC'} selected{/if}>{lang}wcf.global.sortOrder.descending{/lang}</option>
+ </select>
+ </dd>
+ </dl>
+
+ <div class="formSubmit">
+ <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s">
+ </div>
+ </div>
+ </form>
+ </section>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+ <div class="paginationTop">
+ {content}
+ {pages print=true assign=pagesLinks controller='PersonList' link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}
+ {/content}
+ </div>
+{/hascontent}
+
+{if $items}
+ <div class="section sectionContainerList">
+ <ol class="containerList personList">
+ {foreach from=$objects item=person}
+ <li>
+ <div class="box48">
+ <span class="icon icon48 fa-user"></span>
+
+ <div class="details personInformation">
+ <div class="containerHeadline">
+ <h3><a href="{$person->getLink()}">{$person}</a></h3>
+ </div>
+
+ {hascontent}
+ <ul class="inlineList commaSeparated">
+ {content}{event name='personData'}{/content}
+ </ul>
+ {/hascontent}
+
+ {hascontent}
+ <dl class="plain inlineDataList small">
+ {content}
+ {if $person->enableComments}
+ <dt>{lang}wcf.person.comments{/lang}</dt>
+ <dd>{#$person->comments}</dd>
+ {/if}
+
+ {event name='personStatistics'}
+ {/content}
+ </dl>
+ {/hascontent}
+ </div>
+ </div>
+ </li>
+ {/foreach}
+ </ol>
+ </div>
+{else}
+ <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+ {hascontent}
+ <div class="paginationBottom">
+ {content}{@$pagesLinks}{/content}
+ </div>
+ {/hascontent}
+
+ {hascontent}
+ <nav class="contentFooterNavigation">
+ <ul>
+ {content}{event name='contentFooterNavigation'}{/content}
+ </ul>
+ </nav>
+ {/hascontent}
+</footer>
+
+{include file='footer'}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/tornado/userGroupOption.xsd">
+ <import>
+ <categories>
+ <category name="mod.person">
+ <parent>mod</parent>
+ </category>
+
+ <category name="user.person">
+ <parent>user</parent>
+ </category>
+ </categories>
+
+ <options>
+ <!-- admin.content -->
+ <option name="admin.content.canManagePeople">
+ <categoryname>admin.content</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <!-- /admin.content -->
+
+ <!-- mod.person -->
+ <option name="mod.person.canModerateComment">
+ <categoryname>mod.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="mod.person.canEditComment">
+ <categoryname>mod.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="mod.person.canDeleteComment">
+ <categoryname>mod.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <moddefaultvalue>1</moddefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <!-- /mod.person -->
+
+ <!-- user.person -->
+ <option name="user.person.canAddComment">
+ <categoryname>user.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <enableoptions>user.person.canAddCommentWithoutModeration</enableoptions>
+ <excludedInTinyBuild>1</excludedInTinyBuild>
+ </option>
+ <option name="user.person.canAddCommentWithoutModeration">
+ <categoryname>user.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <excludedInTinyBuild>1</excludedInTinyBuild>
+ </option>
+ <option name="user.person.canEditComment">
+ <categoryname>user.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>1</defaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <option name="user.person.canDeleteComment">
+ <categoryname>user.person</categoryname>
+ <optiontype>boolean</optiontype>
+ <defaultvalue>0</defaultvalue>
+ <admindefaultvalue>1</admindefaultvalue>
+ <usersonly>1</usersonly>
+ </option>
+ <!-- /user.person -->
+ </options>
+ </import>
+</data>