diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..db0936738 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1016 @@ +# Change-Log + +- + New +- ✓ Fixed +- Δ Changed +- − Removed + +## [5.5.0] - 2019-11-16 + +- [Full commit-log](https://github.com/Schlaefer/Saito/compare/5.4.1...5.5.0) +- [Download release-zip](https://github.com/Schlaefer/Saito/releases/download/5.5.0/saito-release-master-5.5.0.zip) + + +### Changes + +- + Adds `CHANGELOG.md` to keep track of changes +- + Rewritten and expanded permission system: + - + New, more fine grained permissions + - + Permissions are configurable + - + New role "Owner" +- Uploader: + - + Shows progress-bar when uploading a file + - + Shows speed, time remaining and file size when uploading a file + - + Adds button for canceling the current file-upload + - + Cancel a running upload if the upload-dialog is closed + - + Checks that file with same name isn't uploaded before upload starts + - + Improved responsive layout +- ✓ Fixes user's can't log-out if forum is installed in a subdirectory +- ✓ Fixes login redirect issues if forum is installed in a subdirecotry +- Δ Improves performance of background task runner +- Internal code changes: + - Δ Increases phpstan static code analysis from level 3 to 4 + - Δ Changes passing of current-user throughout the app + - Δ Updates aura/di from 2.x to 4.x + +### Update Notes + +#### Extended Permission System + +Saito 5.0.0 introduced a new permission system which was rewritten and considerably extended in this release. + +##### Configuration + +The configuration is exposed at `config/permissions.php` now. + +Want to allow moderators to contact a user no matter the user's contact-settings? You can do that. Want to disable new registrations? You can do that. Want to allow users to change their email-address? You can do that. And a lot more. + +Permissions are intended to offer flexibility by tweaking the exiting forum behavior to your needs. While possible it is not recommended to start a brand new permission-configuration from scratch. + +If you make changes in `config/permissions.php` don't forget to carry them over if you update to new releases in the future. + +##### The Owner Account + +This update introduces a new user-role *Owner*. The following changes apply to the default configuration: + +- On new installations the first account created is an Owner instead of an Administrator +- The Owner lives "above" the Administrator inheriting all their rights +- The "lower" roles are not allowed to change the role, block or delete an Owner +- Only an Owner can promote (or demote) a user to Administrator or Owner + +The update is not going to change accounts on existing installations and, because this is the whole point, it isn't possible to promote an account to Owner from an Administrator account. To promote an user on an existing installation execute manually in the database: + +```SQL +UPDATE users SET user_type='owner' WHERE username='TheUserName'; +``` + +##### "Lock User" Setting + +The setting for enabling user-locking is removed from the admin-backend and controlled by permissions now. The default behavior is unchanged: moderators may lock, locking status is visible to every user. + +## [5.4.1] - 2019-10-20 + +### Noteworthy Changes + +- ✓ Changing a user name isn't reflected in search results or "edited by" information +- ✓ Improves reliability of executing background maintenance tasks +- ✓ Fixes internal error caused by read-postings garbage collection for registered users +- Δ Improved performance of read-postings garbage collection for registered users + +### Update Notes + +Don't miss to add: + +- [the new "long" cache configuration](https://github.com/Schlaefer/Saito/blob/7d085ea43598cd3220438d7ca6a5169cae2eaf6c/config/app.php#L170) in `config/app.php` +- [the new "logInfo" configuration](https://github.com/Schlaefer/Saito/blob/7d085ea43598cd3220438d7ca6a5169cae2eaf6c/config/saito_config.php#L95) in `config/saito_config.php` + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.4.0...5.4.1) + +## [5.4.0] - 2019-10-12 + +### Noteworthy Changes + +- + Inserts an additional whitespace after closing BBCode tag #360 +- + Improves mime-type detection in Uploader + - + Workaround for [issue with .mp3 files on Chromium-derivates](https://bugs.chromium.org/p/chromium/issues/detail?id=227004) + - + Workaround for .mp4 videos identifying as `application/octet-stream` +- ✓ Fixes issues where errors-messages were displayed without theme +- ✓ Fixes issues where an API-error didn't result in a proper error-response +- Code improvement: + - + Increases TypeScript check to "strict" #355 + - Δ Migrates more Javascript code to TypeScript fixing some minor bugs on the way. + - Δ Migrates user-authentication from [AuthComponent](https://book.cakephp.org/3.0/en/controllers/components/authentication.html) (deprecated in CakePHP 4) to [newer and future-proof Authenticaton-plugin](https://github.com/cakephp/authentication) #361 + +### Update Notes + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.3.3...5.4.0) + +## [5.3.3] - 2019-09-21 + +### Noteworthy Changes + +- ✓ Fixes issues that prevent editing a posting as moderator + +### Update Notes + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.3.2...5.3.3) + +## [5.3.2] - 2019-09-06 + +### Noteworthy Changes + +- Δ Smiley menu is placed below menu buttons in posting form #349 + +### Update Notes + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.3.1...5.3.2) + +## [5.3.1] - 2019-09-01 + +### Noteworthy Changes + +- ✓ Category order of select input in posting form is wrong #345 +- ✓ Force browser to load an updated language .json file #346 +- ✓ 5.3 updater fails on pre 5.2 installations if uploads without title exist #347 +- ✓ Editing a posting doesn't trigger an autoresize on the textarea #348 + +### Update Notes + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.3.0...5.3.1) + +## [5.3.0] - 2019-08-30 + +### Noteworthy Changes + +#### From the Changelog + +- + Send posting before moving on from posting form #338 +- + Save drafts while composing a new posting +- + Browser warns the user before navigating away from a posting form with input +- + Favicon-indicator shows number of unread postings on background tabs with autoreload #95 +- + New setting `answeringAutoSelectCategory` to control category-selection in posting-form +- − Removes support for embedding new Flash videos (`...`) #326 +- ✓ Uploading PNG images allows double-uploads #343 +- ✓ Fixes several bugs causing Internal Error issues +- ✓ Don't autolink file:// URIs #341 +- ✓ Internal posting-hashtag in parenthesis isn't linked #337 +- Δ Changes the default DB engine for the `entries` table from MyISAM to InnoDB #322 +- Δ Keeping track of online users is more accurate while requiring less resources +- Δ Font files for default theme are served locally instead from Google (everything is served locally now) +- Δ Disables Security component on login #339 +- Δ PHP code maintenance + - Δ Improves code quality so it passes phpstan static code analysis on level 3 (was 1) + - Δ Declares all `src/` and `plugins/` PHP files as strict + - Δ Refactors handling of current user's state +- Δ Core library updates (CakePHP 3.8, TypeScript 3) + +#### Never Lose A Posting Again + +5.3.0 refactors and improves a lot of code including keeping track of the current user and posting a new entry. Both touches important functionality and our oldest code paths (reaching back even before the `git init` of this repository). They accumulated a lot of cruft over the years. + +This was also the occasion to introduce exciting new features: + +In the past sending the posting-form was mainly a simple HTTP POST request. If something went wrong the content was gone. The browser's back button wasn't much of a help. From now on a posting is sent in the background before leaving the posting-form. If there's a server error or a connection problem the user is notified and won't lose the posting staring at a blank page. + +While composing a new posting the content is continuously saved as a draft in the background. On the chance that something is going wrong while composing a posting the draft is restored when the user opens the posting form again. + +### Update Notes + +#### New Setting `answeringAutoSelectCategory` + +There's a new setting `answeringAutoSelectCategory` in `config/saito_config.php`. It allows to select a default category for new postings. + +If `true` the first available category (by category-order and accessibility according to user rights) is preselected as default category in the posting form. If `false` the user is forced to select a category. + +Default: `false` (same behavior as in previous versions). + +#### Changing Entries Table from MyISAM to InnoDB #332 + +This update changes the last and biggest table - containing all postings - from MyISAM to the modern InnoDB database-engine. According to my benchmarks this switch shouldn't impose a major performance impact anymore. + +The updater is going to convert the table automatically, but be aware that your PHP-script runtime is limited on a shared-hoster. The conversion may take several minutes depending on the number of postings and exceed that period. So you might end up sitting in front of a blank page wondering what happened. If your forum contains more than 100.000 postings I recommend converting the table manually before starting the updater. Execute e.g. in phpMyAdmin: + +```sql +ALTER TABLE entries ENGINE=InnoDB; +``` + +As always: Backup your database before performing an update. + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.2.1...5.3.0) + +## [5.2.1] - 2019-07-01 + +### Noteworthy Changes + +- ✓ Deleting an user doesn't properly clean-up the user's postings and leaves the entries table in a dirty state + +### Update Notes + +*An update to 5.2.1 is highly recommended.* - Don't delete an user on version 5.0.0+ before updating to 5.2.1. A manual DB fix is possible and not very complicated, open an issue if you ran into this issue and require assistance. + +For a quick in-place upgrade just update `src/Model/Table/EntriesTable.php` and ` src/Lib/version.php`. + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.2.0...5.2.1) + +## [5.2.0] - 2019-07-13 + +### Noteworthy Changes + +5.2 is a feature update with a considerably enhanced uploader and quality of life improvements for user management. + +- + Image uploader is extended to a general purpose uploader #325 +- + Privileged users may see the user-account activation status in user-profile and user-list +- + Privileged users may contact normal users even if the user has messaging disabled #336 +- + Privileged users may directly set a user's password #108 +- + Domain info after link takes Public Suffix List into account +- ✓ RSS feed item doesn't show username +- ✓ RSS feed item doesn't show correct date +- ✓ Bootstrap toasts are themed bright in night-theme +- ✓ Bootstrap toasts are placed beneath modal dialogs +- ✓ Domain info after link breaks on URLs with special chars +- ✓ i18n for deleting categories including German l10n +- Δ Increases font size of the default theme +- Δ Updates marionette.js to version 4 + +### Update Notes + +#### Uploader + +Upload-settings in the admin panel have been removed. Write down your settings (max number of uploads per user) before updating. The Uploader is configured in [`config/saito_config.php`](https://github.com/Schlaefer/Saito/blob/5.2.0/config/saito_config.php#L95) now. Individual file-types and file-size per type are configurable. The default settings allow uploading of common Internet media formats (images, audio , video and text-files). + +#### Access-control + +- + `saito.core.user.activate` - See activation status (default-groups: admin) +- + `saito.core.user.password.set` - Change user password (default-groups: admin) +- Δ `saito.core.user.view.contact` becomes `saito.core.user.contact` - Allows viewing contact data and messaging via contact-form (default-groups: admin) + +I forgot to mention the access-control permissions in the 5.0.0 release notes, didn't I? As I said: version 5 was a big update and most of it happened many moons ago. You'll find the meat [here](https://github.com/Schlaefer/Saito/blob/5.2.0/src/Lib/Saito/User/Permission.php). While the forum is still shipping and tested with the default administrator, moderator, user, and anonymous groups, it is possible to configure those groups - or create your own if you feel adventurous. + + +[Full change-log](https://github.com/Schlaefer/Saito/compare/5.1.0...5.2.0) + +## [5.1.0] - 2019-06-13 + +### Noteworthy Changes + +5.1.0 is a bugfix and maintenance release. + +- + bumps minimum required PHP Version from 7.1 to 7.2+ +- ✓ Creating bookmarks not working on new 5.0.0 installation #334 +- Δ Updating removes database compatibility to Saito 4.10 #323 #324 +- Δ Default database charset and collation for new installation changes from utf8 to utf8mb4 #333 +- Δ Rewritten installer #335 +- Δ Refactored user-blocking internals +- Δ Updates libraries (esp. CakePHP 3.7 and Bootstrap 4.3) + +### Update Notes + +Utf8mb4 is required for full Emoji-support. Existing installation have to update the table and columns to utf8mb4 manually. If you're fine without Emoji support just set `encoding => 'utf8'` as connection parameter for the dabase in `config/app.php` and everything is going to work as before. + +See [full change-log](https://github.com/Schlaefer/Saito/compare/5.0.0...5.1.0) or the [milestone](https://github.com/Schlaefer/Saito/issues?utf8=%E2%9C%93&q=milestone%3A5.1.0+) + +## [5.0.0] - 2019-06-10 + +### What's new + +Hello! + +Saito 5 is big rewrite of major parts of the forum. 90% of the work took place in late 2015, but I burned out at the end, so it didn't make it out of the door. The remaining 9.9% were done in the first half of 2018. 2019 sees the release – finally. + +On the backend the update from CakePHP 2.x to CakePHP 3.x is the most noteworthy, which was a considerable effort. + +The frontend-stack moved from bower, RequireJS, Marionette 1.x and Javascript over to yarn, webpack, Marionette 3.x with parts starting to migrate to Typescript. The UI is based on Bootstrap now, which should offer a more accessible theming-environment. + +Overall there's a stronger separation between frontend and backend. The major theme is that the PHP-backend is going to provide a new JSON based API with JWT authentication which is accessed by a independent frontend JS application. The rewritten image-uploader and bookmarks features being the first incarnations of this transition. + +Future-proving the code-base was the main goal, but there are also feature changes. + +Users are able to set an avatar image now. The layout is better optimised for mobile devices. Category-access-rights are more fine grained. The image-uploader is rewritten and improved (auto-rotate images by EXIF-metadata, remove medata, compress images, thumbnails on index page). The posting form is a custom implementation, which allows more flexibility (sub-paragraph citations, better dialogs for inserting content and esp. for smilies). Embedding of rich 3rd-party content doesn't rely on an external provider anymore. + +On the other hand less popular features didn't made the transition: Shoutbox, community-map, separate mobile-version, email-notifications on answers, admin-stats, … + +### Update + +#### Migrating from 4.x + +Saito 5.0.0 requires PHP 7.1 but is able to run on the same DB as Saito 4.10 (meaning that the DB updates for Saito 5 don't break 4.10). This allows you to move from PHP 5 to 7 with 4.10 and gently switch to Saito 5. + +Saito 5 includes an automated database updater, so no more manually updating the DB with raw SQL commands. *Yeah!* **But** ... I can only hope that you applied the manual steps of the past by the letter. I also assume that your database structure is in the same state as a vanilla 4.10 installation. The automated updater may fail if it isn't ... + +***Please do a database-backup before updating!*** – *This is not a drill!* + +[The database connection](https://book.cakephp.org/3.0/en/orm/database-basics.html#configuration) is set in `config/app.php`. Enter your existing security salt there too. + +There's no support for table prefixes anymore. If prefixes were used in the past rename the tables to an unprefixed version. + +#### Theming + +The new default theme "Bota" replaces "Paz". It is implemented as a [CakePHP 3 theme plugin](https://book.cakephp.org/3.0/en/views/themes.html) and lives in `plugins/Bota`. The UI is implemented as [Bootstrap 4](https://getbootstrap.com/docs/4.1/getting-started/introduction/) theme. + +To start your own theme I recommend using SASS and referencing and customizing the default theme. + +``` +// e.g. in "plugins/YourTheme/webroot/css/src/theme.scss" +// set YourTheme in config/saito_config.php + +//// Change Bootstrap variables + +$body-color: #222; +... + +//// Include the main theme which will pick up the Bootstrap variable values + +@import "../../../../../plugins/Bota/webroot/css/src/theme"; + +//// Additional customizations tweaking the default theme + +@import "_your_customizations.scss"; + +body { + // more customizations +} +``` + +Otherwise you have to bring your own Bootstrap-theme and layout additional forum properties from scratch. + +## [4.1.10] - 2019-06-10 + +### What's new + +- ✓ Fixes incorrect table setup by the installer + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.10.0...4.10.1) + +## [4.10.0] - 2019-05-09 + +### What's new +- + PHP 7.1 compatibility +- + Prepares update to Saito 5 +- + adds new API-endpoint `users/online` +- ✓ fixes smaller issues on MariaDB +- ✓ [mobile] fixes smilies und BBCode in Shoutbox +- ✓ [mobile] fixes issues on login and logout +- ✓ [mobile] fixes bugs when editing a posting starting a thread +- ✓ [mobile] fixes issues when trying to view non-existing threads +- Δ if embed.ly is disabled existing `[embed]`-tags will present a HTML-link + +```sql +ALTER TABLE `user_blocks`CHANGE `by` `blocked_by_user_id` int(11) unsigned NULL DEFAULT NULL; +RENAME TABLE `user_read` TO `user_reads`; + +ALTER TABLE `settings` ADD `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; +INSERT INTO `settings` (`name`, `value`) VALUES ('db_version', '4.10.0'); + +ALTER TABLE `users` CHANGE `user_signatures_images_hide` `user_signatures_images_hide` TINYINT(1) NOT NULL DEFAULT '0'; +``` + +It is possible your settings table already has an "id"-column. In that case make sure it's auto-increment and add a "db_version" key with value "4.10.0" manually. + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.8.0...4.10.0) + +## [4.9.0] + +Unreleased and rolled into 4.10.0. + +## [4.8.0] - 2015-11-18 + +- \+ show remaining chars for subject #312 +- \+ set default format for youtube video fallback from 4:3 to 16:9 #316 +- \+ add [quote] BBCode tag #317 +- \+ show PHP-info in admin panel +- ✓ [mobile] improved reliability when starting the mobile app +- ✓ [mobile] app data isn't updated on Internet Explorer +- ✓ improve [float] BBCode-tag +- ✓ improve embed.ly embedding +- Δ relax CSRF protection when creating new postings +- Δ update CakePHP from 2.6.7 to 2.6.12 + +## [4.7.5] - 2015-11-15 + +### What's new +- ✓ caches were not cleared out on certain operations +- ✓ hide other users signature images not working #315 +- ✓ accession check on categories not always applied +- ✓ improved localization +- Δ update CakePHP from 2.6.3 to 2.6.7 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.7.4...4.7.5) + +## [4.7.4] - 2015-03-21 + +### What's new +- ✓ don't include complete web pages with embedly #314 +- ✓ posting in mobile app not working #313 +- Δ update to CakePHP 2.6.3 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.7.3...4.7.4) + +## [4.7.3] - 2015-02-28 + +### What's new +- + log user-agent in `saito-<*>.log` files +- ✓ maps where not working because of API change on mapquest.com +- ✓ user is not shown in userlist-slidetab #307 +- ✓ don't show ?mar in URL for non-aMAR users +- ✓ improves german localisation +- Δ updates CakePHP from 2.6.0 to 2.6.2 +- Δ minor refactoring + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.7.2...4.7.3) + +## [4.7.2] - 2015-01-10 + +### What's new +- ✓ HTML-entities created by BBCode-parser followed by a parenthesis trigger wink smiley #311 + +Minor code refactoring. + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.7.1...4.7.2) + +## [4.7.1] - 2015-01-04 + +### What's new +- ✓ cite button in answering form doesn't insert text #308 (was bug in flattr-plugin) +- Δ Update CakePHP to 2.6.0 #309 +- Δ Update jQuery to 2.1.3 #310 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.7.0...4.7.1) + +## [4.7.0] - 2014-12-13 + +### What's new +- + Set sort order for non-logged-in users to last-answer #304 +- + add drop shadow to simley-popup in entries/add #303 +- ✓ fix bullet CSS in bookmark index #298 +- ✓ fix badges (via plugin) margin #301 +- ✓ fix default citation mark in bbcode doc #302 +- ✓ fix timing in test case #305 +- Δ rename table column Smilies.order to Smilies.sort #300 +- Δ rename table column Entry.category to Entry.categories_id #299 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.6.0...4.7.0) + +### Migration Notes + +_Note:_ If you use a table prefix you have to prepend it to the table name. + +``` mysql +ALTER TABLE `entries` CHANGE `category` `category_id` INT(11) NOT NULL DEFAULT '0'; +ALTER TABLE `smilies` CHANGE `order` `sort` INT(4) NOT NULL DEFAULT '0'; +``` + +## [4.5.0] - 2014-11-08 + +### What's new +- ✓ fixes an issue when composer wasn't able to find the pear CakePHP package +- ✓ fixes path issue when installing on MS Windows +- ✓ fixes PostgreSQL support +- Δ refactors BBCode-renderer into a plugin (included and activated by default) + - ✓ fixes @Username is not linked before linebreak + - \- removes [u] underline BBCode tag + - \- removes `.c-bbcode-<#>` CSS-classes +- Δ CSS class `.staticPage` was renamed to `.richtext` +- Δ composer root is now in `app/` +- \- removes plugins Flattr, NsfwBadge and Userranks (see Migration Notes) + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.4.0...4.5.0) + +### Migration Notes + +#### Set Parser + +Set the parser in your `saito_config.php`. Default is: + +``` php +Configure::write('Saito.Settings.ParserPlugin', 'Bbcode'); +``` + +which points to `app/Plugin/Parser`. + +#### Plugin Source + +The removed plugins have their own repositories now: +- https://github.com/Schlaefer/saito-flattr (composer: schlaefer/saito-flattr) +- https://github.com/Schlaefer/saito-nsfwbadge (composer: schlaefer/saito-nsfwbadge) +- https://github.com/Schlaefer/saito-userranks (composer: schlaefer/saito-userranks) + +Download them manually and put them into `app/Plugin` or install them via composer. + +## [4.4.0] - 2014-10-26 + +### What's new +- + adds hooks for extending the core (see `docs/dev-hooks.md`) +- ✓ quote symbol set in admin-settings is ignored +- Δ refactors user-ranks + - \- removes user-ranks from core (still available as example plugin, see `app/Plugins/Userranks`) +- Δ refactors flattr support + - \- removes flattr from core (still available as plugin, see `app/Plugins/Flattr`) + - ✓ no flattr button on user-profile +- Δ refactors "Not Safe For Work"-badge + - \- removes NSFW-badge from core (still available as plugin, see `app/Plugins/NsfwBadge`) +- Δ refactors user-blocking + - + automatically unblock blocked users after a specified time + - + moderators and admins see blocking history in user-profile + - + admins see global blocking history in admin-area +- Δ refactors smiley handling + - + introduces new HDPI-ready smiley icons in default theme + - + allows localization of smiley-titles + - Δ changes default smiley-set + - Δ allows usage of pixel or font based smilies +- Δ changes quote symbol for new installations from `»` to `>` + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.5...4.4.0) + +### Migration Notes + +#### DB Changes + +_Note:_ If you use a table prefix you have to prepend it to the table name. + +``` sql +CREATE TABLE `user_blocks` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + `user_id` int(11) unsigned NOT NULL, + `reason` varchar(255) DEFAULT NULL, + `by` int(11) unsigned DEFAULT NULL, + `ends` datetime DEFAULT NULL, + `ended` datetime DEFAULT NULL, + `hash` char(32) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `ends` (`ends`), + KEY `user_id` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; +``` + +#### Remove Userranks + +If you don't activate Userranks again remove its the DB entries: + +``` mysql +DELETE FROM `settings` WHERE `name` IN ('userranks_show'); +DELETE FROM `settings` WHERE `name` IN ('userranks_ranks'); +``` + +#### Remove Flattr + +Remove the old flattr config, its now set in in the Flattr plugin `config.php`: + +``` mysql +DELETE FROM `settings` WHERE `name` IN ('flattr_category','flattr_enabled','flattr_language'); +``` + +If you don't activate Flattr again you should remove its existing DB entries: + +``` mysql +ALTER TABLE `users` DROP `flattr_allow_posting`; +ALTER TABLE `users` DROP `flattr_allow_user`; +ALTER TABLE `users` DROP `flattr_uid`; + +ALTER TABLE `entries` DROP `flattr`; +``` + +#### Remove "Not Safe For Work"-badge + +If you don't activate the "Not Safe For Work"-badge again you should remove its existing DB entries: + +``` mysql +ALTER TABLE `entries` DROP `nsfw`; +``` + +#### New Smiley-Set + +The easiest way to get the new smiley set is to drop the existing smiley-configuration database tables and recreated them (empty the cache in the admin-area afterwards): + +``` mysql +DROP TABLE IF EXISTS `smilies`; + +CREATE TABLE `smilies` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `order` int(4) NOT NULL DEFAULT '0', + `icon` varchar(100) CHARACTER SET utf8 DEFAULT NULL, + `image` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL, + `title` varchar(255) CHARACTER SET utf8 DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +INSERT INTO `smilies` (`id`, `order`, `icon`, `image`, `title`) +VALUES + (1, 1, 'happy', NULL, 'smilies.t.smile'), + (2, 2, 'grin', '', 'smilies.t.grin'), + (3, 3, 'wink', '', 'smilies.t.wink'), + (4, 4, 'saint', '', 'smilies.t.saint'), + (5, 5, 'squint', '', 'smilies.t.sleep'), + (6, 6, 'sunglasses', '', 'smilies.t.cool'), + (7, 7, 'heart-empty-1', '', 'smilies.t.kiss'), + (8, 8, 'thumbsup', '', 'smilies.t.thumbsup'), + (9, 9, 'coffee', NULL, 'smilies.t.coffee'), + (10, 10, 'tongue', '', 'smilies.t.tongue'), + (11, 11, 'devil', NULL, 'smilies.t.evil'), + (12, 12, 'sleep', '', 'smilies.t.blush'), + (13, 13, 'surprised', NULL, 'smilies.t.gasp'), + (14, 14, 'displeased', '', 'smilies.t.embarrassed'), + (15, 15, 'unhappy', '', 'smilies.t.unhappy'), + (16, 16, 'cry', '', 'smilies.t.cry'), + (17, 17, 'angry', '', 'smilies.t.angry'); + + +DROP TABLE IF EXISTS `smiley_codes`; + +CREATE TABLE `smiley_codes` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `smiley_id` int(11) NOT NULL DEFAULT '0', + `code` varchar(32) CHARACTER SET utf8 DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +INSERT INTO `smiley_codes` (`id`, `smiley_id`, `code`) +VALUES + (1, 1, ':-)'), + (2, 1, ':)'), + (3, 2, ':-D'), + (4, 2, ':D'), + (5, 3, ';-)'), + (6, 3, ';)'), + (7, 4, 'O:]'), + (8, 5, '(-.-)zzZ'), + (9, 6, 'B-)'), + (10, 7, ':-*'), + (11, 8, ':grinw:'), + (12, 9, '[_]P'), + (13, 9, ':coffee:'), + (14, 10, ':P'), + (15, 10, ':-P'), + (16, 11, ':evil:'), + (17, 12, ':blush:'), + (18, 13, ':-O'), + (19, 14, ':emba:'), + (20, 14, ':oops:'), + (21, 15, ':-('), + (22, 15, ':('), + (23, 16, ':cry:'), + (24, 16, ':\'('), + (25, 17, ':angry:'), + (26, 17, ':shout:'); +``` + +Otherwise you have to make the changes in the admin area. + +If you want to stick with the old icons: don't change anything and copy over the smilies theme folder from the previous version. + +## [4.3.5] - 2014-10-21 + +### What's new +- ✓ fixes broken entries/edit form on validation error + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.4...4.3.5) + +## [4.3.4] - 2014-10-10 + +### What's new +- ✓ fixes slidetab reordering is not stored on the server +- ✓ fixes some caches are not persistently cleared out +- ✓ fixes a performance regression caused by erroneously cleared caches when adding/editing a posting +- Δ only show small notice if search words are too short + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.3...4.3.4) + +## [4.3.3] - 2014-10-09 + +### What's new +- ✓ fixes showing wrong category in posting tree + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.2...4.3.3) + +## [4.3.2] - 2014-10-09 + +### What's new +- +autofocus first text field in search +- ✓ fixes no recent postings on profile page of ignored users +- ✓ fixes ignored postings are shown in mix view +- ✓ fixes auto-link in [url] BBCode-tag +- ✓ fixes no admin edit of user profile page because of similar name already exists +- Δ shows ignored postings as invisible but clickable placeholders +- Δ update to CakePHP 2.5.5, jQuery 2.1.1 and latest require.js +- Δ code refactoring + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.1...4.3.2) + +## [4.3.1] - 2014-09-28 + +### What's new +- ✓ fixes issues when posting an answer with Safari Mobile + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.3.0...4.3.1) + +## [4.3.0] - 2014-09-27 + +### What's new +- + make ignored postings more flexible by using a CSS `.ignored` class #287 +- + improves detection for password autofill +- + prevents iframe embedding by setting `X-Frame-Options` header +- + help pages open in new window +- ✓ improves blackholed behavior and documentation #286 +- Δ move "Advanced Search"/"Simple Search" navigation to navbar #288 +- Δ refactors thread-tree and mix-tree rendering + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.2.1...4.3.0) + +## [4.2.1] - 2014-09-14 + +### What's new +- + show postings in profile of ignored user #280 +- ✓ default search order is not applied in users/index #282 +- ✓ "Neu Antwort" in german l10n email notification #279 +- ✓ deleting category in admin backend fails #285 +- ✓ creating new category in admin panel doesn't empty category cache #284 +- Δ update to CakePHP 2.5.4+ #283 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.2.0...4.2.1) + +## [4.2.0] - 2014-09-01 + +### What's new +- + ignore users #276 +- + performance improvements in mix view +- ✓ i10n in contacts/<*> headers #277 +- ✓ adds missing back-links in contact form +- ✓ cache prefix not set for default cache #278 +- Δ switch thread cache from whole threads to thread-lines #275 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.1.0...4.2.0) + +### Migration Notes + +#### DB Changes + +_Note:_ If you use a table prefix you have to prepend it to the table name. + +``` mysql +CREATE TABLE `user_ignores` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + `user_id` int(11) DEFAULT NULL, + `blocked_user_id` int(11) DEFAULT NULL, + `timestamp` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `blocked_user_id` (`blocked_user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; + +ALTER TABLE `users` ADD `ignore_count` int(10) unsigned NOT NULL DEFAULT '0'; +``` + +## [4.1.0] - 2014-08-08 + +### What's new + +This release improves user experience for non-logged-in users by providing a MAR. This may increase server load. +- + Mark As Read for anonymous users #274 +- + link to help-page source in help-page footer +- + requests (view, mix) of non-public posting asks for login to access that posting instead of redirecting to homepage +- + on registration a new username must at least two characters off to any existing username to be available +- ✓ fixes dummy_data shell +- Δ refactors contact code + - Δ URL to contact admin changes from `/users/contact/0` to `/contacts/owner/` + - Δ URL to contact users changes from `/users/contact/` to `/contacts/user/` + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.5...4.1.0) + +## [4.0.5] - 2014-07-27 + +### What's new +- ✓ fixes [e] BBCode-tag bleeds into following content #270 +- ✓ ongoing CSRF blackholing in 4.0.4 #269 +- Δ optimizes composer autoload performance in release build #272 +- Δ updates jBBCode to 1.3 #271 +- Δ updates CakePHP to 2.5.3 +- Δ code refactoring + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.4...4.0.5) + +## [4.0.4] - 2014-07-05 + +### What's new +- + switches BBCode parser to jBBCode + - Δ changes languages selection in [code]-tag from `[code ]` to `[code=]` (no backwards compatibility/breaks existing BBCode) +- + less strict security settings to prevent overly eager CSRF-blackholing +- + adds vine to to allowed video domains +- + makes simple search available for non-logged in users +- + performance improvements +- ✓ fixes new threads don't show up in recent entries s(l)idebar +- ✓ fixes orphaned entries in `user_read` table +- ✓ fixes no eng. l10n for markitup link-popup +- ✓ breaks long words in slidetab to next line +- Δ updates CakePHP to 2.5.2 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.3...4.0.4) + +## [4.0.3] - 2014-06-17 + +### What's new +- ✓ improves word-length-detection in simple-search + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.2...4.0.3) + +## [4.0.2] - 2014-06-10 + +### What's new +- ✓ blank page when changing password #266 +- ✓ tab behavior in register and login form broken #267 +- ✓ log blackholed requestes #268 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.1...4.0.2) + +## [4.0.1] - 2014-06-08 + +### What's new +- + clear localStorage on logout #262 +- ✓ includes jasmine js test in cli test runner #242 +- ✓ internal error if categories are activated on user profile for the first time #263 +- ✓ layout Category popup gobbeld #265 +- ✓ improves Category popup positioning +- ✓ Sending Category form is blackholed #264 + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.0...4.0.1) + +## [4.0.0] - 2014-05-29 + +### What's new + +All changes of 4.0.0-RC - 4.0.0-RC3 and +- ✓ Inline Answering not working with SecurityComponent enabled #261 +- ✓ blob → mediumblob conversion for ecaches table was not applied in installer +- Δ changes default cookie encryption to AES +- Δ deactivates form autofill in login and registration form + +[Full change-log](https://github.com/Schlaefer/Saito/compare/4.0.0-RC3...4.0.0) + +## [4.0.0-RC3] - 2014-05-18 + +### What's new +- ✓ fixes logout not working +- ✓ fixes non collapsing back links in responsive design +- Δ Updates CakePHP to 2.5.1 + +[See full change-log.](https://github.com/Schlaefer/Saito/compare/4.0.0-RC2...4.0.0-RC3) + +## [4.0.0-RC2] - 2014-05-16 + +### What's new +- ✓ thread cache isn't checked appropriately and reads/saves wrong output +- ✓ skip not implemented and failing pgsql simple search test case +- Δ code refactoring + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/4.0.0-RC...4.0.0-RC2) + +## [4.0.0-RC1] - 2014-04-29 + +### What's new +- + Don't update view counter on search engine robots #243 +- + extended crawler/robots detection +- + improves autolinking of URLs next to punctuation marks +- + shows used cache engines in system info admin panel +- + add doc link and "where to edit" info to /users/map #247 +- + sort threads by last answer is default now +- + /users/index is always sorted alphabetically after primary sort parameter +- + PostgreSQL support #259 (except for simple search) +- ✓ show date on older shoutbox entries #251 +- ✓ absolute date in mobile view is gobbled #170 +- ✓ limit map boundaries and minimum zoom-level +- ✓ [bbcode] urls are not parsed in lists #256 +- ✓ theme error on /users/edit on validation error #244 +- ✓ deleting last bookmark should show "no bookmarks" message #75 +- ✓ installation creates `BLOB` instead of `MEDIUMBLOB` field in `ecaches` table +- ✓ global help button is not activated on answering form +- ✓ i18n decimal divider in generation time +- ✓ fixes no pointer cursor on .btn-strip hover +- ✓ Double entries in UserOnline slidebar #157 +- ✓ accession 1 entries-url should not be in sitemap +- Δ rewritten user login #254 + - + shows info if user account is not activated yet + - + autofill username on failing login in login-form + - + autofocus/select username in login-form + - + log failing logins +- Δ rewritten user registration #253 + - + shows info if sending of confirmation email failed + - + log if sending of confirmation email failed + - + shows info if confirmation link failed + - + shows info if account was already activated + - + adds navigation back-links to registration views + - Δ l10n changes + - !confirmation-URL in activation-email changed +- Δ rewritten user change password #255 + - + log change attempts for non-existing users + - + log change attempts by non-authorized users +- Δ refactors bookmark edit + - + log edit attempts by non-authorized users +- Δ refactors contact messaging + - + advanced email address configuration #223 + - + show disclaimer on global contact form + - + adds navigation back-link + - + logs if sending of contact email failed +- + CakePHP `dummy_data` shell to generate artificial content for development +- Δ changes disclaimer l10n strings +- Δ Update to CakePHP 2.5.0 #246 +- Δ replaces underscore.js with lo-dash +- Δ activate CakePHP's SecurityComponent by default +- Δ renames log file `auth.log` to `saito-auth.log` +- Δ add staticPage layout for all `pages` esp. TOS #257 +- Δ consolidates database field types +- Δ consolidates database index names +- Δ removes unused database fields + +Other bugfixes and improvements. This updates includes important security enhancements. + +### Migration Notes + +#### DB Changes + +_Note:_ If you use a table prefix you have to prepend it to the table name. + +_Note:_ Depending on DB-size these may run some time. Make a DB backup and apply separately. + +``` mysql +DROP TABLE IF EXISTS `useronline`; + +CREATE TABLE `useronline` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(32) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', + `user_id` int(11) DEFAULT NULL, + `logged_in` tinyint(1) NOT NULL, + `time` int(14) NOT NULL DEFAULT '0', + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `useronline_uuid` (`uuid`), + KEY `useronline_userId` (`user_id`), + KEY `useronline_loggedIn` (`logged_in`) +) ENGINE=MEMORY AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +ALTER TABLE `bookmarks` DROP INDEX `entry_id-user_id`; +ALTER TABLE `bookmarks` ADD INDEX `bookmarks_entryId_userId` (`entry_id`, `user_id`); + +ALTER TABLE `bookmarks` DROP INDEX `user_id`; +ALTER TABLE `bookmarks` ADD INDEX `bookmarks_userId` (`user_id`); + +ALTER TABLE `entries` DROP INDEX `user_id`; +ALTER TABLE `entries` ADD INDEX `entries_userId` (`user_id`); + +ALTER TABLE `entries` DROP INDEX `user_id-time`; +ALTER TABLE `entries` ADD INDEX `entries_userId_time` (`time`, `user_id`); + +ALTER TABLE `entries` CHANGE `last_answer` `last_answer` TIMESTAMP NULL DEFAULT NULL; +UPDATE `entries` SET last_answer=NULL WHERE last_answer='0000-00-00 00:00:00'; + +ALTER TABLE `entries` CHANGE `edited` `edited` TIMESTAMP NULL DEFAULT NULL; +UPDATE `entries` SET edited=NULL WHERE edited='0000-00-00 00:00:00'; + +ALTER TABLE `users` CHANGE `last_login` `last_login` TIMESTAMP NULL DEFAULT NULL; +UPDATE `users` SET last_login=NULL WHERE last_login='0000-00-00 00:00:00'; + +ALTER TABLE `users` CHANGE `registered` `registered` TIMESTAMP NULL DEFAULT NULL; + +ALTER TABLE `users` CHANGE `last_refresh` `last_refresh` TIMESTAMP NULL DEFAULT NULL; +UPDATE `users` SET last_refresh=NULL WHERE last_refresh='0000-00-00 00:00:00'; + +ALTER TABLE `users` CHANGE `last_refresh_tmp` `last_refresh_tmp` TIMESTAMP NULL DEFAULT NULL; +UPDATE `users` SET last_refresh_tmp=NULL WHERE last_refresh_tmp='0000-00-00 00:00:00'; + +ALTER TABLE `users` CHANGE `personal_messages` `personal_messages` TINYINT(1) NOT NULL DEFAULT '1'; +ALTER TABLE `users` CHANGE `user_lock` `user_lock` TINYINT(1) NOT NULL DEFAULT '0'; +ALTER TABLE `users` CHANGE `user_signatures_hide` `user_signatures_hide` TINYINT(1) NOT NULL DEFAULT '0'; +ALTER TABLE `users` CHANGE `user_automaticaly_mark_as_read` `user_automaticaly_mark_as_read` TINYINT(1) NOT NULL DEFAULT '1'; +ALTER TABLE `users` CHANGE `user_sort_last_answer` `user_sort_last_answer` TINYINT(1) NOT NULL DEFAULT '1'; +ALTER TABLE `users` CHANGE `show_recententries` `show_recententries` TINYINT(1) NOT NULL DEFAULT '0'; +ALTER TABLE `users` CHANGE `user_category_custom` `user_category_custom` VARCHAR(512) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL; + +ALTER TABLE `users` CHANGE `activate_code` `activate_code` INT(7) NOT NULL DEFAULT '0'; + +ALTER TABLE `entries` DROP `uniqid`; +ALTER TABLE `users` DROP `last_logout`; +ALTER TABLE `users` DROP `hide_email`; +ALTER TABLE `users` DROP `user_show_own_signature`; + +INSERT INTO `settings` (`name`, `value`) VALUES ('email_contact', NULL); +INSERT INTO `settings` (`name`, `value`) VALUES ('email_register', NULL); +INSERT INTO `settings` (`name`, `value`) VALUES ('email_system', NULL); +``` + +_If_ you're using MySQL and the field `value` in the `ecaches` table is of type `BLOB` change it to `MEDIUMBLOB`: + +``` mysql +ALTER TABLE `ecaches` CHANGE `value` `value` MEDIUMBLOB NOT NULL; +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.5.1...4.0.0-RC) + + + +## Older Changes + +See [CHANGELOG_OLD.md](docs/CHANGELOG_OLD.md) for older changes. diff --git a/build.xml b/build.xml index d4990abe4..6741aa68d 100644 --- a/build.xml +++ b/build.xml @@ -18,6 +18,7 @@ + diff --git a/composer.json b/composer.json index 1c2646392..aee46a4f7 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "josegonzalez/dotenv": "*", "mobiledetect/mobiledetectlib": "2.*", - "aura/di": "^2.2.5", + "aura/di": "^4.0", "davidyell/proffer": "^1.0", "jbbcode/jbbcode": "~1.4", "markstory/geshi": "^3", @@ -99,12 +99,11 @@ "cs-check": "phpcs --runtime-set ignore_warnings_on_exit true", "cs-fix": "phpcbf > /dev/null || true", - "check": ["@cs-fix", "@cs-check"], + "php-check": ["@phpstan", "@cs-fix", "@cs-check"], "phpstan": "vendor/bin/phpstan analyse --ansi", "coverage": [ - "unset XDEBUG_CONFIG", "Composer\\Config::disableProcessTimeout", - "composer phpunit -- --coverage-html docs/local/" + "unset XDEBUG_CONFIG; composer phpunit -- --coverage-html docs/local/" ], "phpunit-stop": [ "Composer\\Config::disableProcessTimeout", @@ -117,8 +116,7 @@ "test": [ "unset XDEBUG_CONFIG", "@phpunit", - "@phpstan", - "@check" + "@php-check" ], "js-all": "yarn run test", diff --git a/composer.lock b/composer.lock index 6c01da003..2194566d1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,31 +4,35 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4fe3322e84051ba16558d0c8bd7d5df2", + "content-hash": "99f1b718749b0447db98a4a4a2414566", "packages": [ { "name": "aura/di", - "version": "2.2.5", + "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/auraphp/Aura.Di.git", - "reference": "e485b235d4c3841a7e8959158ac7e69f425a52c5" + "reference": "ea4b166e9006babca411732c4d45ee14efa32259" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/auraphp/Aura.Di/zipball/e485b235d4c3841a7e8959158ac7e69f425a52c5", - "reference": "e485b235d4c3841a7e8959158ac7e69f425a52c5", + "url": "https://api.github.com/repos/auraphp/Aura.Di/zipball/ea4b166e9006babca411732c4d45ee14efa32259", + "reference": "ea4b166e9006babca411732c4d45ee14efa32259", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0", + "psr/container": "^1.0" }, - "type": "library", - "extra": { - "aura": { - "type": "library" - } + "provide": { + "psr/container-implementation": "^1.0" }, + "require-dev": { + "acclimate/container": "^2", + "phpunit/phpunit": "^7", + "producer/producer": "^2.3" + }, + "type": "library", "autoload": { "psr-4": { "Aura\\Di\\": "src/" @@ -36,7 +40,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "MIT" ], "authors": [ { @@ -44,7 +48,7 @@ "homepage": "https://github.com/auraphp/Aura.Di/contributors" } ], - "description": "Provides a dependency injection container system with native support for constructor- and setter-based injection, lazy-loading of services, and inheritable configuration of setters and constructor params.", + "description": "A serializable dependency injection container with constructor and setter injection, interface and trait awareness, configuration inheritance, and much more.", "homepage": "https://github.com/auraphp/Aura.Di", "keywords": [ "container", @@ -53,7 +57,7 @@ "di", "di container" ], - "time": "2018-12-24T14:20:59+00:00" + "time": "2019-04-26T12:07:02+00:00" }, { "name": "aura/intl", diff --git a/config/Migrations/20191013000000_Saito5x5x0.php b/config/Migrations/20191013000000_Saito5x5x0.php new file mode 100755 index 000000000..7681b7248 --- /dev/null +++ b/config/Migrations/20191013000000_Saito5x5x0.php @@ -0,0 +1,16 @@ +execute('DELETE FROM `settings` WHERE `name` IN (\'block_user_ui\')'); + } + + public function down() + { + } +} + diff --git a/config/Seeds/UsersSeed.php b/config/Seeds/UsersSeed.php index cc406b7ff..c89700f7a 100644 --- a/config/Seeds/UsersSeed.php +++ b/config/Seeds/UsersSeed.php @@ -20,7 +20,7 @@ public function run() { $data = [ 'id' => 1, - 'user_type' => 'admin', + 'user_type' => 'owner', 'username' => '__set_by_installer__', 'password' => '__set_by_installer__', 'activate_code' => '0', diff --git a/config/bootstrap.php b/config/bootstrap.php index ec1b5b5ed..6a6fdc2cd 100755 --- a/config/bootstrap.php +++ b/config/bootstrap.php @@ -74,6 +74,7 @@ /** * Load additional config files */ + Configure::load('permissions', 'default'); Configure::load('saito_config', 'default'); Configure::config('saitoCore', new PhpConfig(APP . '/Lib/')); Configure::load('version', 'saitoCore'); diff --git a/config/permissions.php b/config/permissions.php new file mode 100644 index 000000000..4b7d74fda --- /dev/null +++ b/config/permissions.php @@ -0,0 +1,115 @@ +' + */ +$config['Saito']['Permission']['Roles'] = (new Roles) + // Non logged-in visitors + ->add('anon', 0) + // Registered and logged-in users + ->add('user', 1, ['anon']) + // Moderators + ->add('mod', 2, ['user', 'anon']) + // Administrators + ->add('admin', 3, ['mod', 'user', 'anon']) + // Owners + ->add('owner', 4, ['admin', 'mod', 'user', 'anon']); + +/** + * Permissions + * + * everbody > owner > role + */ +$config['Saito']['Permission']['Resources'] = (new Resources()) + /** + * Allow roles access to resource based on roles + */ + // Access to the administration backend + ->add((new Resource('saito.core.admin.backend')) + ->allow((new ResourceAC())->asRole('admin'))) + // Pin or lock a posting + ->add((new Resource('saito.core.posting.pinAndLock')) + ->allow((new ResourceAC())->asRole('mod'))) + // Delete a posting + ->add((new Resource('saito.core.posting.delete')) + ->allow((new ResourceAC())->asRole('mod'))) + // Allow user to edit their own postings + ->add((new Resource('saito.core.posting.edit')) + ->allow((new ResourceAC())->onOwn())) + // "Moderator" mode. Restricted to other user and accessible categories + ->add((new Resource('saito.core.posting.edit.restricted')) + ->allow((new ResourceAC())->asRole('mod'))) + // Allows unrestricted editing of postings + ->add((new Resource('saito.core.posting.edit.unrestricted')) + ->allow((new ResourceAC())->asRole('admin'))) + // Show user's IP address if available + ->add((new Resource('saito.core.posting.ip.view')) + ->allow((new ResourceAC())->asRole('mod'))) + // Merge postings + ->add((new Resource('saito.core.posting.merge')) + ->allow((new ResourceAC())->asRole('mod'))) + // Show a user's activation status + ->add((new Resource('saito.core.user.activate.view')) + ->allow((new ResourceAC())->asRole('admin'))) + // Contact a user no matter their contact settings + ->add((new Resource('saito.core.user.contact')) + ->allow((new ResourceAC())->asRole('admin'))) + // Delete user + ->add((new Resource('saito.core.user.delete')) + ->allow((new ResourceAC())->asRole('mod')->onRole('user')) + ->allow((new ResourceAC())->asRole('admin')->onRoles('mod', 'user')) + ->allow((new ResourceAC())->asRole('owner'))) + // Edit a user's profile page + ->add((new Resource('saito.core.user.edit')) + ->allow((new ResourceAC())->onOwn()) + ->allow((new ResourceAC())->asRole('admin'))) + // Change a user's email address + ->add((new Resource('saito.core.user.email.set')) + ->allow((new ResourceAC())->asRole('admin'))) + // Allows locking-out of users + ->add((new Resource('saito.core.user.lock.set')) + ->allow((new ResourceAC())->asRole('mod')->onRole('user')) + ->allow((new ResourceAC())->asRole('admin')->onRoles('mod', 'user')) + ->allow((new ResourceAC())->asRole('owner'))) + // Show a user's blocking status + ->add((new Resource('saito.core.user.lock.view')) + ->allow((new ResourceAC())->asRole('user'))) + // Change a user's name + ->add((new Resource('saito.core.user.name.set')) + ->allow((new ResourceAC())->asRole('admin'))) + // Change a user's password + ->add((new Resource('saito.core.user.password.set')) + ->allow((new ResourceAC())->asRole('admin')->onRoles('mod', 'user')) + ->allow((new ResourceAC())->asRole('owner'))) + // Change a user's role. Allowed ranks: all the current user has but not + // their own rank. + ->add((new Resource('saito.core.user.role.set.restricted')) + ->allow((new ResourceAC())->asRole('admin')->onRoles('mod', 'user'))) + // Change a user's role. Allowed ranks: all the current user has including + // their own rank. + ->add((new Resource('saito.core.user.role.set.unrestricted')) + ->allow((new ResourceAC())->asRole('owner'))) + // Deleting bookmarks + ->add((new Resource('saito.plugin.bookmarks.delete')) + ->allow((new ResourceAC())->onOwn())) + // Use the register form + ->add((new Resource('saito.core.user.register')) + ->allow((new ResourceAC())->asEverybody())) + ; + +return $config; diff --git a/config/saito_config.php b/config/saito_config.php index 3402beda0..de9452b61 100644 --- a/config/saito_config.php +++ b/config/saito_config.php @@ -125,5 +125,4 @@ ->addType('video/mp4') ->addType('video/webm'); - return $config; diff --git a/dev/docker/php7/docker-compose.yml b/dev/docker/php7/docker-compose.yml index ca6273c2e..6ccc1cf53 100644 --- a/dev/docker/php7/docker-compose.yml +++ b/dev/docker/php7/docker-compose.yml @@ -3,7 +3,7 @@ version: "3.1" services: webserver: build: apache - container_name: saito5-webserver-71 + container_name: saito5-webserver-72 volumes: - ./../../../:/var/www/html ports: diff --git a/docs/CHANGELOG_OLD.md b/docs/CHANGELOG_OLD.md new file mode 100644 index 000000000..e4d6d0a59 --- /dev/null +++ b/docs/CHANGELOG_OLD.md @@ -0,0 +1,1646 @@ +## [3.5.1] - 2014-04-12 + +### What's new +- [fix] link to /users/map in /users/\* area even if maps are not activated #245 +- [fix] map location validation is limited to ([0-90],[0-90]) #249 + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.5.0...3.5.1) + +## [3.5.0] - 2014-04-12 + +### What's new +- [new] Community Map #238 +- [task] consolidates paginator navigation +- [task] updates CakePHP to 2.4.7 #241 +- [task] updates Marionette, Backbone, Underscore to AMD versions +- [task] updates JS test frameworks, but disables CLI tests (see #242) + +### Migration Notes + +The new map feature utilizes MapQuest to render maps. You have to enter a MapQuest API-key in the admin area which you can issue for free at http://developer.mapquest.com/. + +#### DB Changes + +_Note:_ If you use a table prefix you have to prepend it to the table name. + +``` +ALTER TABLE `users` ADD `user_place_lat` FLOAT NULL DEFAULT NULL AFTER `user_place`; +ALTER TABLE `users` ADD `user_place_lng` FLOAT NULL DEFAULT NULL AFTER `user_place_lat`; +ALTER TABLE `users` ADD `user_place_zoom` TINYINT(4) NULL DEFAULT NULL AFTER `user_place_lng`; + +INSERT INTO `settings` (`name`, `value`) VALUES ('map_enabled', '0'); +INSERT INTO `settings` (`name`, `value`) VALUES ('map_api_key', ''); +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.4.1...3.5.0) + +## [3.4.1] - 2014-04-06 + +### What's new +- [fix] internal error on /users/index in 3.4.0 #239 +- [fix] error pages don't have theme applied #240 + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.4.0...3.4.1) + +## [3.4.0] - 2014-14-05 + +### What's new +- [new] improves entry semantic page structure +- [new] show entry view counter only for logged in users +- [new] show category title on every `view` and `mix` subentry +- [new] exclude all `/users/…` pages from `robots.txt` +- [new] detects protocol-less absolute urls in [url] BBCode-tag +- [fix] FE notification messages are not localized +- [fix] sitemap entries improved timing +- [fix] sitemap entries `lastmod` ignores edits +- [fix] missing l10n +- [fix] Paz CSS tweaks +- [task] removes unused database fields + +Overall refactoring and performance improvements. + +### Migration Notes + +#### DB Changes + +Note: If you use a table prefix you have to prepend it to the table name. + +``` +ALTER TABLE `users` DROP `new_posting_notify`; +ALTER TABLE `users` DROP `new_user_notify`; +ALTER TABLE `users` DROP `time_difference`; +ALTER TABLE `users` DROP `user_view`; +ALTER TABLE `users` DROP `pwf_code`; +ALTER TABLE `users` DROP `user_forum_hr_ruler`; +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.3.0...3.4.0) + +## [3.3.0] - 2014-03-30 + +### What's new +- [new] Show number of helpful answers in user-profile #222 +- [new] admin-panel: performance improvements in user list +- [new] Saito installable via composer #232 +- [new] Create sitemap.xml for Entries #20 ] +- [new] additional admin statistics #233 +- [fix] Admin setting Thread Indent Depth is not applied #234 +- [fix] Notification emails are not send #236 (regression from 3.2.0) +- [task] Switch user table from MyISAM to InnoDB #228 +- [task] Update CakePHP to 2.4.6 #231 +- [task] updates robots.txt #235 +- [task] automated dev setup (see `docs/`) #225, #209 + +### Migration Notes + +#### DB Changes + +Note: If you use a table prefix you have to prepend it to the table name. + +For MySQL databases the user table default engine is InnoDB now: + +``` +ALTER TABLE `users` ENGINE = InnoDB; +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.2.1...3.3.0) + +## [3.2.1] - 2014-03-22 + +### What's new +- [fix] Renaming user doesn't empty (thread) cache #221 +- [fix] Don't show prerequisite error on browser prefetch #220 + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.2.0...3.2.1) + +## [3.2.0] - 2014-03-08 + +### What's new +- Search improvements + - [new] sort by "rank" or "time" in simple-search + - [new] error message in simple-search if entered search-term is shorter than DB-config permits + - [new] "jump to first page"-link in search-result navigation + - [fix] Search for username matches substring #154 + - [change] Split simple and advanced search into two separate pages #218 +- [new] Gelesene Beiträge tatsächlich als gelesen markieren. #96 +- [fix] "subject is to long" error #219 +- [change] Deprecate auto sanitizing in AppModel #217 + +### Migration Notes + +#### DB Changes + +Note: Don't forget to add your table prefix if necessary. + +``` +CREATE TABLE `user_read` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `entry_id` int(11) NOT NULL, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `entry_id` (`entry_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.1.2...3.2.0) + +## [3.1.2] - 2014-03-02 + +### What's new +- [fix] Fix failing test case #216 +- [fix] Paz Theme Tweaks #215 +- [fix] smilies admin edit doesn't empty smiley cache #210 +- [task] code refactoring for upcoming features + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.1.1...3.1.2) + +## [3.1.1] - 2014-02-24 + +### What's new + +Notable changes are: +- [fix] Fix new/old heading link color in inline-open and mix-view #214 +- [fix] debug output in page footer should be full page width #211 +- [fix] content of columns Image / Title missing in admin/smilies #87 +- [fix] improves BBCode-tag handling quotes +- [fix] tightens username input validation + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.1.0...3.1.1) + +## [3.1.0] 2014-02-22 + +### What's new + +Notable changes are: +- [new] new help-system #206 +- [new] introduce [e] tag for editing with appropriate icon #205 +- [new] show newest registered user in disclaimer footer #183 +- [new] increase view counter on all entries in thread when thread is viewed in mix view #66 +- [new] Bring back bottom bar at least on entries/index in Paz default theme #201 +- [change] Decrease interface elements and heading size in inline-opened posting (Paz) #203 + +### Migration Notes + +Existing `app/Config/database.php` must be amended with a `$saitoHelp` configuration: + +``` +class DATABASE_CONFIG { + + public $default = array( + … + ); + + public $saitoHelp = [ + 'datasource' => 'SaitoHelp.SaitoHelpSource' + ]; + + … +``` + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.0.2...3.1.0) + +## [3.0.2] - 2014-02-17 + +### What's new + +Notable changes are: +- [new] BBCode tag documentation in [docs/user-bbcodes.md](https://github.com/Schlaefer/Saito/blob/master/docs/user-bbcodes.md) +- [fix] Resolve page heading RSS feeds static page #190 +- [fix] Fix i10n admin panel → create new user #196 +- [fix] finish i10n admin interface #184 +- [fix] Localize Merge Thread Form #60 +- [fix] Registration time for admin in new installation is not set #195 +- [fix] Set database default connection for installation to utf8 #200 +- [fix] Fix failing travis test cases #199 +- [change] swaps Home/Saito Home links in admin menu #40 +- [change] renames common.js, main.js to common.min.js and main.min.js + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.0.1...3.0.2) + +## [3.0.1] - 2014-02-11 + +### What's new + +Bugfix release for 3.0.0. Inline-open "all" performance is greatly improved. + +Notable changes are: +- [new] resolution independent and i10nable NSFW badge #188 +- [fix] fix loading indicator lockup on inline-open #192 +- [fix] BBCode detaginizer must preprocess Geshi content #193 +- [fix] Center uploaded images #189 +- [fix] finish i10n entries/add #185 +- [fix] CSS for prerequisite warnings is missing #197 +- [change] consolidate Paz theme JS #198 +- [change] Update to CakePHP 2.4.5 #180 + +### Links +- [Full Changelog](https://github.com/Schlaefer/Saito/compare/3.0.0...3.0.1) + +## [3.0.0] 2014-02-07 + +### What's new +- [new] new Default Theme Paz +- [new] changes named parameter `stopwatch:true` to GET request parameter `?stopwatch=true` +- [new] changes named parameter `lang:` to GET request parameter `?lang=` +- [new] completes english i10n in users/edit +- [new] dynamically resizes textarea in posting form +- [new] adds [float] BBCode tag +- [fix] comment text cut off in bookmarks/index +- [fix] fixes special char encoding when editing an existing bookmark comment +- [fix] fixes "open new" button not working/shown +- [fix] fixes internal hash links not working in BBCode-lists +- [change] signature is not longer shown below n/t- or in inline-opened postings +- [change] displays shorter time format for older entries +- [change] minor layout refinements in search-, login- and register-form +- [change] minor layout adjustments to users/view +- [change] refines answering locked button design +- [change] updates core libraries (jQuery UI 1.10.4) +- [remove] Bootstrap is no longer available in user front-end +- [remove] removes buggy 'thread with new entry' bullet indicator +- [remove] deactivates help menu + +## [2.0.1] 2014-01-27 + +### What's new +- [fix] fixes failing test cases + +## [2.0.0] 2014-01-17 + +### What's new + +[Complete changelist](https://github.com/Schlaefer/Saito/compare/2013-11.06...2.0.0). Noteworthy changes: +- [new] #174 Add user-preference theme chooser (for configuring see `app/Config/saito_config.php`) +- [new] #173 Remove user font-size customization +- [new] retina smiley button image +- [new] retina button for image upload delete +- [new] icon for HTML5 notification is located in `webroot/img/html5-notification-icon.png` now +- [new] Shoutbox performance improvements +- [new] `title_for_page` and `forum_name` variables available in view files +- [new] search result navigation buttons are moved into navigation bar +- [new] changes version string to semantic versioning +- [new] updates core libraries (#177 CakePHP 2.4.4, #178 jQuery 2.1, #179 Marionette 1.5+, require.js 1.9.10) +- [new] updates layout in users/index +- [fix] Shoutbox input field is two lines high in Firefox +- [fix] #172 No code toggle icon for geshi +- [fix] #171 Email notifications are not filtered by recipient === author +- [task] #175 Change GET theme selector from named param to standard query-string + +Code refactoring esp. CSS class name cleanup in preparation of new default theme. + +### Migration Notes + +#### Theme + +Theme is recompiled. Note esp. the new `
` wrapper in `default.ctp`. + +#### DB Changes + +Note: Don't forget to add your table prefix if necessary. + +``` +ALTER TABLE `users` DROP `user_font_size`; +ALTER TABLE `users` ADD `user_theme` VARCHAR(255) NULL DEFAULT NULL; +``` + +## 2013-11.06 ## + +### What's new + +- [new] show warning if JavaScript is not available +- [new] show warning if localStorage is not available +- [fix] don't timeout DOM if localStorage is not available +- [fix] layout tweaks +- [task] updates CakePHP to 2.4.3 +- [task] makes auth cookie http only + +### Migration notes + +Theme is recompiled and uses compass 0.13.alpha.10+ instead of 0.12. + +## 2013-11.05 ## + +### What's new ### + +- [new] show latest log entries in admin area +- [fix] help popup not showing +- [task] refactors status and language asset requests +- [task] layout tweaks + +### Migration notes ### + +Theme is recompiled. + +## 2013-11.04 ## + +### What's new ### + +- [new] marks new shoutbox entries on per browser basis +- [fix] sets user online if the page is open (temp. workaround for regression) +- [fix] adds time until Stopwatch startup to Stopwatch::getWallTime +- [fix] excludes search button from sequential tab focus +- [task] CSS tweak + +Code refactoring. + +## 2013-11.03 ## + +### What's new ### + +- [new] #165 Add edit-time onto edit button +- [new] Stopwatch outputs warm-up times in debug output +- [new] improves responsive layout +- [new] mobile customization documentation in `docs/config-customizing.md` +- [fix] mobile: fixes mobile view only using default theme assets +- [task] refactors edit time from .ctp-template into .js-view + +### Migration Notes ### + +#### Theme #### + +Theme is recompiled. + +#### default.ctp #### + +If you use a custom `default.ctp` file replace: + +
+
+
+ element('layout/header_login'); ?> +
+ + +with + +
+ + +
+
+ element('layout/header_login'); ?> +
+
+ +## 2013-11.02 ## + +### What's new ### + +- [new] #166 Invalidate mobile cache manifest on `Empty Caches` button + +## 2013-11.01 ## + +### What's new ### + +- [new] reduces minimum page width to 768px +- [new] PHP 5.5 support +- [fix] [mobile] long links overflow content area +- [task] refactors Bookmark and Solves buttons in posting form +- [task] #163 - implements Server Side Events for status messages (disabled by default) +- [task] updates to font-awesome 4, marionette 1.2.2, backbone 1.1 + +Code formatting and refactoring. + +## 2013-10.03 ## + +### What's new ### + +- [new] #9 thread-starter can mark helpful answers +- [new] Shoutbox notifications if window is in background +- [new] Shoutbox JSON-API +- [fix] #162 Special chars in email notifications are html encoded +- [task] rewritten Shoutbox consuming the API +- [task] adds several new grunt commands: `grunt test`, `grunt compass:watch`, `grunt compass:compile` +- [task] adds phpcs, jasmine, jshint tests to `grunt test` +- [task] updates CakePHP to 2.4.2 +- [task] code cleanup and refactoring + +Notifications use the image `[Theme/]webroot/img/apple-touch-icon-precomposed.png` as icon. + +### Regressions ### + +- no shoutbox in mobile view + +### DB Changes ### + + Note: Don't forget to add your table prefix if necessary. + + ALTER TABLE `entries` ADD `solves` INT( 11 ) NOT NULL DEFAULT '0'; + +## 2013-10.02 ## + +### What's new ### + +- [fix] #158 Checkboxes for email notifications not working on new entries. +- [fix] missing remaining time on submit button when editing +- [task] move frontend components to bower +- [task] use grunt for js dev-setup and release generation + +## 2013-10.01 ## + +### What's new ### + +- [new] mobile view "[Togusa](http://en.wikipedia.org/wiki/Togusa)" +- [fix] iframe overflow on viewing entries + +## 2013-09.07 ## + +### What's new ### + +- [new] [API] `shoutbox_enabled` attribute in bootstrap + +## 2013-09.06 ## + +### What's new ### + +- [fix] fixes mandatory category select in search form +- [fix] [API] always logout on login attempt + +Code refactoring. + +## 2013-09.05 ## + +### What's new ### + +- [fix] fixes invalid html +- [fix] [hr]/[---] don't need a close tag anymore +- [fix] media overflow in inline opening + +## 2013-09.04 ## + +### What's new ### + +- [fix] fixes some issues in entries/edit and entries/add + +## 2013-09.03 ## + +### What's new ### + +- [fix] resovles thread tree sort ambiguity if entries have same time +- [fix] no upload form in entries/add +- [fix] performance improvement in entries/view + +## 2013-09.02 ## + +### What's new ### + +- [fix] improves "double click to send twice"-protection in entry form +- [fix] show ValidityState messages in entry form + +Code refactoring. + +## 2013-09.01 ## + +### What's new ### + +- [new] JSON API for basic forum functionality (login, add, edit, logout) +- [new] [s] as shorthand for [strike] bbcode-tag +- [new] Smiley model is cached for better performance +- [new] Updates CakePHP to 2.4.1 +- [fix] fixes missing whitespaces in [spoiler] text + +Code refactoring and performance improvements. + +There's a new JSON API which is optional at the moment. See `docs/api-v1.md` for more information. + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + INSERT INTO `settings` (`name`, `value`) VALUES ('api_enabled', '1'); + INSERT INTO `settings` (`name`, `value`) VALUES ('api_crossdomain', ''); + +## 2013-08.03 ## + +### What's new ### + +- [new] new [spoiler] bbcode tag with button +- [new] removed [u] underline bbcode button +- [new] unified bbcode buttons for image and media +- [fix] #150 Upload not working in iCab Mobile +- [task] updates CakePHP to 2.4.0 + +Code refactoring. + +### Migration notes ### + +Don't forget to update your `lib/Cake` folder. + +## 2013-08.02 ## + +### What's new ### + +- [fix] makes youtu.be video insert protocol relative +- [task] includes require.js in main-prod.js +- [task] updates jQuery from 2.0.2 to 2.0.3 +- [task] updates CakePHP to 2.4.0-RC2 +- [task] updates font-awesome to 2.3.1 +- [task] updates require.js to 2.1.8 +- [task] updates r.js to 2.1.8 +- [task] updates text.js to 2.0.10 + +### Migration notes ### + +This version uses a Release Candidate version of CakePHP. Stay at 2013-06.05 for a stable release. + +Don't forget to update your `lib/Cake` folder. + +## 2013-08.01 ## + +### What's new ### + +- [new] improved navigation in admin settings +- [fix] user ranks in admin settings are not shown +- [fix] changing category title in admin settings doesn't empty thread cache +- [fix] can't save entry if subject is exactly max length (regression from 2013-07.01) +- [fix] shoutbox is rendered twice on page load +- [task] updates to CakePHP2.4-RC1 + +Significant code refactoring and minor performance improvements. + +### Migration notes ### + +This version uses a Release Candidate version of CakePHP. Stay at 2013-06.05 for a stable release. + +Don't forget to update your `lib/Cake` folder. + +## 2013-07.01b ## + +### What’s New ### + +- [fix] answering in admin category fails + +### Migration notes ### + +This version uses a beta version of CakePHP. Stay at 2013-06.05 for a stable release. + +## 2013-07.01a ## + +### What’s New ### + +- updates to latest CakePHP 2.4 dev version +- temporary fix for + +### Migration notes ### + +This version uses a beta version of CakePHP. Stay at 2013-06.05 for a stable release. + +Don't forget to update your `lib/Cake` folder. + +## 2013-07.01 ## + +### What’s New ### + +- [fix] #148 encoding in [code] block not working +- [task] update to CakePHP 2.4beta + +Code refactoring. Performance and security improvements. + +### Migration notes ### + +This version uses a beta version of CakePHP. + +Don't forget to update your `lib/Cake` folder. + +## 2013-06.05 ## + +### What’s New ### + +- [new] performance improvements (settings are now cached) +- [fix] email addresses in admin user index are link instead of mailto + +## 2013-06.04 ## + +### What’s New ### + +- [new] performance improvements on entries/mix + +## 2013-06.03 ## + +### What’s New ### + +- [new] email addresses are obfuscated in entry output +- [new] minor performance improvements in entries/mix +- [new] all plugins in app/Plugins are now loaded automatically +- [fix] #147 [fixes #147 Middle click not working in Firefox +][gh147] +- [fix] fixes html5 validation related errors on entries/add in firefox +- [fix] corner radius on mod button not effected by theme settings +- [task] update to CakePHP 2.3.6 +- [task] refactored auto-mark-as-read +- [task] consolidated twitter-bootstrap include + +[gh147]: https://github.com/Schlaefer/Saito/issues/147 + +### Migration Notes ### + +#### Misc #### + +Theme is recompiled. + +#### default.ctp #### + +If you use a custom `default.ctp` file replace: + + Html->link( + $this->Html->image( + 'forum_logo.png', array( 'alt' => 'Logo', 'height' => 70 ) + ), + '/', + array( 'id' => 'btn_header_logo', 'escape' => false )); + ?> + +with + + Html->link( + $this->Html->image( + 'forum_logo.png', + ['alt' => 'Logo', 'height' => 70] + ), + '/' . (isset($markAsRead) ? '?mar' : ''), + $options = [ + 'id' => 'btn_header_logo', + 'escape' => false, + ] + ); + ?> + + +## 2013-06.02 ## + +### What's new ### + +- [fix] #145 [recent entries/postings slidetabs don't update immediately if APC is used for caching][gh145] +- [fix] #146 [alignment in recent entries/postings is off][gh146] + +[gh146]: https://github.com/Schlaefer/Saito/issues/146 +[gh145]: https://github.com/Schlaefer/Saito/issues/145 + +### Migration notes ### + +Theme is recompiled. + +## 2013-06.01 ## + +### What's new ### + +- [new] performance improvements esp. rendering flat (and long) thread trees +- [new] new bbcode icon for source code +- [new] new layout for posting entries +- [new] when answering show parent entry's subject as placeholder in empty subject +- [fix] #144 [Deleting a user doesn't empty entry cache][gh144] +- [fix] #143 [users/view/ should have more descriptive title tag][gh143] +- [task] replaced blueprint with susy CSS framework +- [task] updated jQuery to 2.0.2 +- [task] updated fontawesome to 3.1 +- [task] code refactoring + +[gh144]: https://github.com/Schlaefer/Saito/issues/144 +[gh143]: https://github.com/Schlaefer/Saito/issues/143 + +### Migration notes ### + +Theme is recompiled. + +## 2013-05.04 ## + +### What's new ### + +- [new] link to user entry search at bottom of recent changes in /users/view +- [new] minor performance improvements using hashlinks +- [fix] #NumberChar tags are hashlinked +- minor code refactoring + +## 2013-05.03 ## + +### What's new ### + +- [fix] #141 [Can't delete user: confirmation dialog doesn't stay on screen][gh141] +- [fix] #142 [Same origin js problem with embed.ly and twitter tweets][gh142] +- [task] minor code refactoring + +[gh141]: https://github.com/Schlaefer/Saito/issues/141 +[gh142]: https://github.com/Schlaefer/Saito/issues/142 + +## 2013-05.02 ## + +### What's new ### + +- [fix] #140 [page content times out (content not shown, js not initalized)][gh140] +- [task] update to CakePHP 2.3.5 +- [task] minor code refactoring + +[gh140]: https://github.com/Schlaefer/Saito/issues/140 + +## 2013-05.01 ## + +### What's new ### + +- [fix] fixed some performance regressions introduced in 2013-04.x + +## 2013-04.08 ## + +### What's new ### + +- [task] update to CakePHP 2.3.4 +- [task] minor code refactoring + +## 2013-04.07 ## + +### What's new ### + +- [fix] don't scroll on mass inline openings (all, new) + +## 2013-04.06 ## + +### What's new ### + +- [new] Shoutbox shows content on page load +- [new] rendered Shoutbox html is cached for better performance +- [new] improved click responsiveness on iOS +- [new] Retina magnifier icon in search field +- [fix] #139 [Reverse Hash Link Not Working][gh139] +- [fix] no scroll-into-view on inline opening (regression from 2013–04.01) +- [task] updated bootstrap to 2.3.1 +- [task] updates jQuery to 2.0.0 +- [task] CSS and HTML cleanup + +[gh139]: https://github.com/Schlaefer/Saito/issues/139 + +### Migration Notes ### + +jQuery 2.0.0 drops support for IE 8 and below. + +## 2013-04.05 ## + +### What's new ### + +- [new] redesigned users/index +- [fix] 1970 timestamp if shoutbox has no shouts +- [fix] removed console.log() call from preview +- [task] CSS and HTML cleanup +- [task] switch to sass-twitter-bootstrap + +### Migration notes ### + +Recompile theme if necessary. + +Instead of depending on the `bootstrap-sass` gem to be installed for compass compiling `sass-twitter-bootstrap` is included now in `app/Vendor`. + + +## 2013-04.04 ## + +### What's new ### + +- [fix] no reverse hashing of entries/view links +- [task] update from CakePHP 2.3 to 2.3.2 + +### Migration notes ### + +Don't forget to update your `lib/Cake` folder. + +## 2013-04.03 ## + +### What's new ### + +- [fix] multimedia button not working + +## 2013-04.02 ## + +### What's new ### + +- [new] insert images via multimedia button +- [new] automatically convert fubar dropbox links to dl.dropbox.com (See: ) if multimedia button is used +- [new] performance improvements +- [fix] no notifications in admin area +- [task] code cleanup + +### Migration notes ### + +Theme is recompiled. + +## 2013-04.01 ## + +### What's new ### + +This is a release with major code refactoring. + +- [new] uploader with drag & drop panel +- [new] short tag # links to entry +- [new] short tag @ links to user profile +- [new] show peak memory in stopwatch debug output +- [new] new url for named profiles: users/name/ +- [new] sets meta description in entries/view of n/t postings to subject of that posting +- [new] required min PHP version is now 5.4 +- [fix] #102 [No text format within a list][gh102] +- [fix] #19 [raw urls in code werden zu url geparst][gh19] +- [fix] improved layout and scrolling behavior in uploader on tablets +- [fix] slidetabs don't remember state if installed in server root +- [fix] no pinch & zoom on iPad +- [fix] show source code button broken in inline open +- [fix] shift+tab in entries/add textarea now working +- [fix] hard browser reload don't remember page position +- [fix] shoutbox doesn't load when first opened +- [fix] relative local links in bbcode don't work if server port is not `80` +- [task] new notification and messaging system +- [task] changed doc format from xhtml to html5 +- [task] refactored all remaining js classes and files into backbone +- [task] updated backbone.js to 1.0 +- [task] i18n for js frontend +- [task] more test cases +- [task] reactivated Selenium test cases which are now using the CakePHP data fixtures +- [task] cleaned up html tree structure and reduced number of html tags +- [task] added `youtube-nocookie` domain to trusted video domains (installer) + +[gh19]: https://github.com/Schlaefer/Saito/issues/9 +[gh102]: https://github.com/Schlaefer/Saito/issues/102 + +### Theme ### + +#### default.ctp #### + +If you use a custom `default.ctp` layout remove the following lines from it: + + Session->flash(); + $emailMessage = $this->Session->flash('email'); + if ($flashMessage || $emailMessage) : + ?> +
+ + +
+ + +Replace the line: + + if (!SaitoApp.request.isPreview) { $('#content').hide(); } + +with: + + if (!SaitoApp.request.isPreview) { $('#content').css('visibility', 'hidden'); } + + + +#### styles.scss #### + +There is a new `css/src/base/_uploads.scss` file. It has to be included in in `/webroot/css/src/styles.scss`: + + @import "base/_uploads"; + +## 2013-02.03 ## + +- [fix] errors using Camino + +## 2013-02.02 ## + +- [new] #129 [Slidetab should only be sortable by dragging the slidebar tab][gh129] (allows selecting text in sidebar) +- [fix] #127 [Stop autoreload if text is entered in shoutbox texfield][gh127] +- [fix] subject is required in advanced search form +- [fix] info counter on user slidetab tab needs page reload to show/hide +- [task] refactored slidetab js code into backbone +- [task] refactored help dialog js code into backbone + +[gh127]: https://github.com/Schlaefer/Saito/issues/127 +[gh129]: https://github.com/Schlaefer/Saito/issues/129 + +## 2013-02.01 ## + +- [new] Shoutbox +- [new] display PHP peek memory usage in debug output +- [fix] #123 Alt-Tags broken in Edit-Window +- [fix] #130 show raw [code] option broken +- [fix] #131 [embed] is not excluded from bbcode parsing if multimedia is set to false +- [fix] #132 Usercounter on slidetab misalligned +- [fix] js-tests can be run in production mode +- [fix] mark as read button is get link and pollutes browser history +- [fix] wobbeling word baseline in [code] blocks +- [task] #124 Update to CakePHP 2.3 +- [task] #125 Update to jQuery 1.9.1 +- [task] updated backbone.js (underscore, localStorage) +- [task] updated require.js (domReady, text) +- [task] unified layout center button in header and footer +- [task] refactored bookmark page js from page template into backbone +- [task] refactored scroll to top footer button js into backbone +- [task] unified layout center button in header and footer +- [task] migrated js test (yes, we have some) from qunit to jasmine +- [task] updated markItUp to patched version for jQuery 1.9+ that doesn't need jQuery.migrate +- [task] removed jQuery.migrate +- [task] basic email config info in docs/config-email.md + + +[Milestone issues.](https://github.com/Schlaefer/Saito/issues?milestone=10&state=closed) + + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + INSERT INTO `settings` (`name`, `value`) VALUES ('shoutbox_enabled', '1'); + INSERT INTO `settings` (`name`, `value`) VALUES ('shoutbox_max_shouts', '10'); + + CREATE TABLE `shouts` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + `text` varchar(255) CHARACTER SET utf8 DEFAULT NULL, + `user_id` int(11) NOT NULL, + `time` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MEMORY AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; + + ALTER TABLE `users` ADD `show_shoutbox` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `show_recententries` + + +## 2013-01.05 ## + +- [fix] iOS issues with buttons in posting form + +[Milestone issues.](https://github.com/Schlaefer/Saito/issues?milestone=9&state=closed) + +## 2013-01.04 ## + +- [new] Sort admin usertable by last registrations by default +- [fix] Up arrow in footer misaligned + +[Milestone issues.](https://github.com/Schlaefer/Saito/issues?milestone=8&state=closed) + +## 2013-01.03 ## + +- [fix] surpress jQuery.migrate warnings in production mode +- [fix] thread pre icons as utf8 instead of fontawesome + +## 2013-01.02 ## + +- [fix] iOS scrolling performance regression from 2013-01.01 +- [fix] thread close icon position iOS + +## 2013-01.01 ## + +- [new] Updated core libraries (CakePHP 2.3 RC2, jQuery 1.9, jQuery UI 1.9, markItUp 1.1.13, fontawesome 3.0) +- [new] SMTP option for sending emails #107 +- [new] thread-icons in (theme) CSS instead of hardcoded in PHP +- [new] layout tweaks + +[Complete list.](https://github.com/Schlaefer/Saito/issues?milestone=7&state=closed) + +## 2012-12.03 ## + +- [fix] media embedding broken + +## 2012-12.02 ## + +- [new] user option to collapse threads by default +- [fix] empty preview in Safari top sites + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + ALTER TABLE `users` ADD `user_show_thread_collapsed` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT '0' AFTER `inline_view_on_click` + + +## 2012-12.01 ## + +### What's new ### + +- [new] layout tweaks +- [new] updated core from CakePHP 2.3 beta to CakePHP 2.3 RC1 +- [fix] timeout for content ready +- [fix] subject field not focused + +### Update Note + +The CakePHP core is updated to a release candidate version. If you don't trust it, leave this release out. If you update don't forget the `lib/Cake` folder. + +## 2012-11.05 ## + +### What's new ### + +- [new] Updated core to CakePHP 2.3 beta +- [new] usercounter on closed usersidebar +- [new] performance improvements esp. in mix-view +- [new] simple statistics panel in admin area +- [new] layout tweaks +- [fix] wait until JS is fully initialized before showing page content + + +### Update Note + +The CakePHP core is updated to a beta version. If you don't trust it, leave this release out. If you update don't forget the `lib/Cake` folder. + +Recompile your theme if necessary. + +Please note the change in default.ctp from: + +
+ fetch('content'); ?> +
+ +to: + +
+ + fetch('content'); ?> +
+ + +## 2012-11.04 ## + +### What's new ### + +- [new] robuster BBCode on https installations +- [new] updated jQuery 1.8.1 to 1.8.3 +- [new] admin and debug-tools use bundled js libraries instead of CDN +- [new] view user pofile by /users/view/<username> +- [fix] refined layout contact form +- [fix] mark confirm new password field as mandatory + +## 2012-11.03 ## + +### What's new ### + +- [new] option to send message copy to sender in contact form +- [new] anonymous user has to provide an email address in contact form +- [new] moderators can remove an arbitrary entry and its subentries (a.k.a. delete subthread) +- [task] code cleanup & refactoring; passing test cases for php 5.4 + +## 2012-11.02 ## + +### What's new ### + +- tweaked simple search + +## 2012-11.01 ## + +### What's new ### + +- set Sender field in email messages +- workaround for WebKit bug 101443 + +### Update Note + +Recompile your theme if necessary. + +## 2012-10.03 ## + +### What's new ### + +- [fix] can't access admin area + +## 2012-10.02 ## + +### What's new ### + +Code refactoring. + +## 2012-10.01 ## + +### What's new ### + +- [fix] escape special chars after displaying an inline answer +- [fix] changing category on root entry changes category on all entries in thread +- [fix] when merging threads change category of appended entries to target category +- [fix] Ignore Safari preview request in auto-mark-as-read + +## 2012-09.07 ## + +### What's new ### + +- [new] #100 [delete new registered but not activated users automatically after 24 hrs][gh100] +- [fix] #51 [Collapsed thread in entries/index also collapsed in entries/view][gh51] +- [fix] incorporate server timezone in admin user index +- [fix] widen search field in users/ pages + +[gh51]: https://github.com/Schlaefer/Saito/issues/51 +[gh100]: https://github.com/Schlaefer/Saito/issues/100 + +## 2012-09.06 ## + +### What's new ### + +- [fix] Missing users in users/index +- [fix] Warning messages on entries/mix when thread doesn't exist + +## 2012-09.05 ## + +### What's new ### + +- [new] #98 [Improve detailed search by adding category filter][gh98] +- [new] #99 [Nachbearbeitungszeitpunkt um Datum erweitern][gh99] +- [fix] scrolling tweaks +- [fix] layout tweaks +- [fix] SEO tweaks + +[gh98]: https://github.com/Schlaefer/Saito/issues/98 +[gh99]: https://github.com/Schlaefer/Saito/issues/99 + +## 2012-09.04 ## + +### What's new ### + +- [new] make sure newest CSS and JS is used by browser (no more cache emptying after update) +- [fix] inline-opening with option "always open inline" fails after inline-answer (also #94 [Error message "Posting not found"][gh94]) +- [fix] throw error when trying to view non-existing thread in entries/mix + +[gh94]: https://github.com/Schlaefer/Saito/issues/94 + +### Notes + +In your `app/Config/core.php` change + + Configure::write('Asset.timestamp', true); + +to + + Configure::write('Asset.timestamp', 'force'); + + +## 2012-09.03 + +### What's new + +- [fix] autoreload not working if forum is installed in webroot +- [fix] some minor notices blowing up the debug.log + +## 2012-09.02 + +### What's new + +- [new] robots.txt in webroot (thanks to kt007) +- [new] don't count (popular) search engine crawlers as guests +- [new] disables autoreload if an inline answering form was opened +- [new] set html title tag in entries/mix to subject of root posting +- [fix] search performance regression introduced in b28e8de71dbd6f8f45909caa374dfa5c7aa74c3e +- [fix] cleaned up headers and breadcrump navigation in admin interface +- [fix] tweaked inline-opening handling +- [fix] german l10n typos (thanks to Schnaks) +- [fix] automatically mark as read more robust on new sessions +- [fix] new entries are marked read on autoreload +- [task] updated jQuery to 1.8.1 +- [task] javascript refactoring + +## 2012-09.01 + +### What's new + +- [new] "Empty Caches" button in admin panel +- [new] performance improvements +- [fix] layout tweaks in /users/edit/# +- [fix] refresh time stepper allows values below zero +- [fix] #89 [New entry instead of reply with deactivated JS][gh89] +- [fix] no search results for username if Entry.name is empty +- [fix] open new entries button is shown for not logged-in users +- [fix] Missing localization for entries in mod menu +- [task] Javascript refactoring + +[gh89]: https://github.com/Schlaefer/Saito/issues/89 + +## 2012-08.07 + +### What's new + +- [new] reduced recent user postings in s(l)idetab from 10 to 5 +- [new] /users/contact/0 contacts email adress specified in admin forum settings +- [fix] use forum_disabled.ctp from current Theme folder +- [fix] #18 [remove macnemo favicon][gh18] +- [fix] #84 [Uncached threads always show the showNewThreads-Button][gh84] +- [task] #11 [forum_disabled.ctp entnemofizieren][gh11] +- [task] #83 [rename 'Alles' to 'Alle Kategorien' for category chooser +][gh83] +- [task] javascript refactoring + +[gh11]: https://github.com/Schlaefer/Saito/issues/11 +[gh18]: https://github.com/Schlaefer/Saito/issues/18 +[gh83]: https://github.com/Schlaefer/Saito/issues/83 +[gh84]: https://github.com/Schlaefer/Saito/issues/84 + +### Theme Changes + +Contact adress in disclaimer.ctp is now `/users/contact/0` (was `/users/contact/1`). + +## 2012-08.06 + +### What's new + +- [new] change language with `lang:` url parameter on the fly +- [fix] #82 [Pin and Lock menu don't send ajax call when openend inline +][gh82] +- [fix] #81 [Performing Un-/pin and Un-/lock in mod menu removes icon][gh81] +- [fix] no editing and user's homeplace information in entries/mix +- [fix] no pin icon in entries/[view|mix] +- [task] implemented s(l)idetabs using view blocks +- [task] Entry code refactoring +- [task] Auth code cleanup + +[gh81]: https://github.com/Schlaefer/Saito/issues/81 +[gh82]: https://github.com/Schlaefer/Saito/issues/82 + + +### Theme Changes + +All CSS `slidebar*` classes were consolidated and renamed to `slidetab*`. + + +## 2012-08.05 + +### What's new + +- [new] significant performance improvements +- [new] plot of stopwatch diff times in debug mode +- [new] #73 [append disclaimer to all page controller pages][gh73] +- [new] improved tab behavior on users/login +- [fix] #77 ["Edit Bookmark" eindeutschen][gh77] + +[gh73]: https://github.com/Schlaefer/Saito/issues/73 +[gh77]: https://github.com/Schlaefer/Saito/issues/77 + +## 2012-08.04 + +### What's new + +- [new] improved caching behavior +- [new] update documentation +- [fix] #72 [Update to jQuery 1.8][gh72] +- [fix] l10n + +[gh72]: https://github.com/Schlaefer/Saito/issues/72 + +## 2012-08.03 + +### What's new + +- [new] bookmarks +- [new] tweaked caching for better performance +- [new] layout tweaks + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + CREATE TABLE `bookmarks` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) unsigned NOT NULL, + `entry_id` int(11) unsigned NOT NULL, + `comment` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '', + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `entry_id-user_id` (`entry_id`,`user_id`), + KEY `user_id` (`user_id`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + CREATE TABLE `ecaches` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `created` datetime NOT NULL, + `modified` datetime NOT NULL, + `key` varchar(128) NOT NULL, + `value` mediumblob NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `key` (`key`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +### Theme Changes + +Update in your default.ctp: + +
+ element('users/login_form'); ?> +
+ +to + + element('users/login_modal'); ?> + + + +## 2012-08.02 + +### What's new + +- [fix] #57 [Bottom of drop down menues is hidden in the inline view of the index][gh57] +- [fix] cascading mod-button in entries/mix/# + +[gh57]: https://github.com/Schlaefer/Saito/issues/57 + +## 2012-08.01 + +### What's new + +- [new] Hide signature separator if signature is empty +- [new] Relative time values in recent entries sidetab +- [new] Layout tweaks +- [fix] hide mod menu in entry/view if menu is empty +- [fix] #64 [Mod menu in users/view/# empty if no mod option][gh64] +- [fix] anonymous user counter shows negative value (-1) +- [fix] Localizations + +[gh64]: https://github.com/Schlaefer/Saito/issues/64 + +## 2012-07.05 + +### What's new + +- [new] if subject is empty when answering use parent's subject + +## 2012-07.04 + +### What's new + +- [new] #67 [Countdown timer in editing form][gh67] +- [fix] #68 [fix admin/users/index sorting for registration date][gh68] + +[gh67]: https://github.com/Schlaefer/Saito/issues/67 +[gh68]: https://github.com/Schlaefer/Saito/issues/68 + + +## 2012-07.03 + +### What's new + +- [new] subject field in answer form is empty by default +- [new] user tab in admin panel +- [fix] add user in admin panel +- [fix] #65 [Space in thread line before posting time][gh65] +- [fix] cleaned up rss/json feed data +- [fix] #63 [Show the last 20 instead of 10 entries in users/view/#][gh63] + +[gh63]: https://github.com/Schlaefer/Saito/issues/63 +[gh65]: https://github.com/Schlaefer/Saito/issues/65 + + +## 2012-07.02 + +### What's new + +- [new] Category chooser on front page + - Admin option to activate for all users + - Admin option to allow users to activate in their user pref +- [new] Term of Service confirmation checkbox on user registration + - Admin option to enable it + - Admin option to provide a custom ToS-url +- [new] #62 Support for embedding .opus files + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + ALTER TABLE `users` CHANGE `activate_code` `activate_code` INT(7) UNSIGNED NOT NULL; + + ALTER TABLE `users` DROP `user_categories`; + ALTER TABLE `users` ADD `user_category_override` TINYINT( 1 ) UNSIGNED NOT NULL AFTER `flattr_allow_posting` , ADD `user_category_active` INT( 11 ) NOT NULL DEFAULT '0' AFTER `user_category_override` , ADD `user_category_custom` VARCHAR( 512 ) NOT NULL AFTER `user_category_active`; + INSERT INTO `settings` (`name`, `value`) VALUES ('category_chooser_global', '0'); + INSERT INTO `settings` (`name`, `value`) VALUES ('category_chooser_user_override', '1'); + + INSERT INTO `settings` (`name`, `value`) VALUES ('tos_enabled', '0'); + INSERT INTO `settings` (`name`, `value`) VALUES ('tos_url', ''); + + +## 2012-07.01 + +### What's new + +- [new] Email notification about new answers to posting or thread +- [new] S(l)idetab recent entries. Shows the 10 last new entries. +- [new] refined users/edit layout (thanks to kt007) +- [new] Mods can merge threads (append thread to an entry in another thread) +- [new] admin forum setting to enable stopwatch output in production mode with url parameter `/stopwatch:true/` +- [new] refactored cache: performance improvements on entries/index/# + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + ALTER TABLE `users` DROP `show_about`; + ALTER TABLE `users` DROP `show_donate`; + + ALTER TABLE `users` ADD `show_recententries` TINYINT( 1 ) UNSIGNED NOT NULL AFTER `show_recentposts`; + + INSERT INTO `settings` (`name`, `value`) VALUES ('stopwatch_get', '0'); + + CREATE TABLE `esevents` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `subject` int(11) unsigned NOT NULL, + `event` int(11) unsigned NOT NULL, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `subject_event` (`subject`,`event`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + CREATE TABLE `esnotifications` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(11) unsigned NOT NULL, + `esevent_id` int(11) unsigned NOT NULL, + `esreceiver_id` int(11) unsigned NOT NULL, + `deactivate` int(8) unsigned NOT NULL, + `created` datetime DEFAULT NULL, + `modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `userid_esreceiverid` (`user_id`,`esreceiver_id`), + KEY `eseventid_esreceiverid_userid` (`esevent_id`,`esreceiver_id`,`user_id`) + ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + + +## 2012-07-08 + +### What's new + +- [new] update to CakePHP 2.2 +- [new] using Rijndael for cookie encryption +- [new] performance improvements on entries/index +- [fix] #56 Editing posting doesn't empty its tree cache. +- [fix] route /login +- [fix] german localization title tag edit buttons + +### Update Note + +Don't forget to update your `lib/Cake` folder. + +Because of the new cookie encryption format permanently logged-in users have to login again to renew their cookie. + +## 2012-06-30 + +### What's new + +- [new] significant performance improvement (less server load) on entries/index +- [fix] Security issue when performing searches +- [fix] can't paginate on entries/index +- [fix] layout: no padding on inline-opened entries + +### DB Changes + +Note: Don't forget to add your table prefix if necessary. + + ALTER TABLE `users` ADD UNIQUE INDEX (`username`); + ALTER TABLE `categories` ADD `thread_count` INT( 11 ) NOT NULL + + +## 2012-06-27 + +- [new] /login shortcut for login-form at /users/login +- [fix] no title-tag on (Category) in /entries/view/# +- [fix] several display glitches on help popups +- [fix] #54 Posting preview contains (Categorie) in headline +- [fix] Minor layout glitches.™ + +## 2012-06-26 + + +### What's new + +- [new] embed.ly support +- [new] /entries/source/#id outputs raw bbcode +- [new] horizontal ruler tag [hr][/hr] with custom shortcut [---] +- [fix] no frontpage caching for logged-out users +- [fix] improved positioning of smiley popup in entries edit form +- [fix] layout tweaks + +### DB Changes: + +Note: Don't forget to add your table prefix if necessary. + + INSERT INTO `settings` (`name`, `value`) VALUES ('embedly_enabled', '0'); + INSERT INTO `settings` (`name`, `value`) VALUES ('embedly_key', NULL); + +### Theme Changes + +Please note that Layouts/default.ctp now includes all JS and CakePHP boilerplate via layout/html_footer.ctp to simplify future updates. + +## 2012-06-24 + +- [new] Admin option to enable moderators to block users +- [new] Admin can delete users +- [new] Admin option to store (anonymized) IPs +- [new] Admin sees user's email adress in users/view/# +- [new] More resolution independent icons +- [new] Password are stored using bcrypt (automatic migration for existing user on next login) +- [new] Support for authentication with mylittleforum 2 passwords +- [new] Notify admin when new users registers (see saito_config file) [testing notification system] +- [fix] #55 German Language files entnemofizieren +- [fix] wrong link on button in entries/view to entries/mix +- [fix] one very long word in subject breaks layout (esp. iPhone) +- [fix] empty parentheses in user/view when user ranks are deactivated +- [fix] Last entries in users/view doesn't respect user's access rights +- [fix] Search doesn't respect user's access rights +- [fix] heavily refactored styles +- [fix] Expanded german and english localization + +DB Changes: + + INSERT INTO `settings` (`name`, `value`) VALUES ('block_user_ui', 1); + INSERT INTO `settings` (`name`, `value`) VALUES ('store_ip', '0'); + INSERT INTO `settings` (`name`, `value`) VALUES ('store_ip_anonymized', '1'); + + ALTER TABLE `entries` ADD `ip` VARCHAR(39) NULL DEFAULT NULL AFTER `nsfw`; + +## 2012-05-16 + +- [new] #53 Use local font files instead of Google Fonts +- [new] [upload] tag accepts `widht` and `height` attribute +- [new] changed html title-tag format from `forumtitle – pagetitle` to `pagetitle – forumtitle` +- [new] ca. server-time spend generating the site displayed in front-page footer +- [new] layout tweaks +- [fix] no Open Sans font on older OS X/Safari versions +- [fix] theoretical issue where users could change each others passwords +- [fix] flattr button now loads its resources via https if the forum itself is running with https (fixes browser error message "insecure content") +- [fix] unofficial support for font-size in user-preferences +- [fix] #52 Wrong comma and username format when viewing posting and not logged-in + +## 2012-05-11 + +- [new] more layout tweaks and css refactoring +- [fix] #45 Replace ? Help-Icon with text. +- [fix] #46 Replace Plus Sign in front of New Entry link with borderless one +- [fix] #49 userranks_show with bogus default value after installation +- [fix] #7 Tooltip für Kategoriensichtbarkeit +- [fix] #47 No drop shadow on video embedding popup + +## 2012-05-06 + +- [new] popup help system +- [new] several layout tweaks +- [fix] missing page-number in title on entries/index +- [fix] vertical back button in mix-view doesn't jump to thread in entries/index +- [task] reimplemented header navigation with cake2.1 view blocks + +## 2012-05-04 + +- [new] more layout tweaks and css refactoring +- [new] more english localizations +- [new] stricter inline-answering: now on front page and in mix view only +- [fix] CakePHP MySQL fulltext index patch for Cake 2.1.2 +- [fix] #43 Unterstrichen [u] funktioniert nicht +- [fix] #42 Kein Inhalt im title-Tag nach Cake 2.1 Update +- [fix] RSS feed (Cake 2 regression) + +## 2012-05-02 + +- [new] update to CakePHP 2.1.2 +- [new] many more layout tweaks +- [new] more english localization +- [new] more resolution independent icons +- [new] admin can change his own password +- [fix] contact admin broken if user is not logged-in +- [fix] shift-tab from entry textarea to subject field broken + + +## 2012-04-24 + +- Dedicated [Saito homepage](http://saito.siezi.com/) +- [new] Updated Default layout with iPad and iPhone optimizations made to macnemo theme in v2012-04-13 +- [new] *Many more* layout tweaks +- [new] New close thread button (client side only) +- [new] Resolution independend icons in navigation bar +- [new] English localization (still incomplete) +- [new] resizable search field in header +- [fix] layout search field with shadow 1px off +- [fix] localized german month names in search form +- [fix] fully localized footer (disclaimer) +- [fix] On iOS Cursors doesn't jump out off subject field anymore + +## 2012-04-13 + +- Update from Cake 1.3 to 2.0 +- Layoutoptimierungen für iPad und iPhone +- Cyrus' iPad Zoom Bug ist (hoffentlich) erschlagen +- Smiliebuttons fügen ein zusätzliches Leerzeichen ein, damit viele nacheinander zusammenklicken kann +- Mods können eigene, angepinnte Beiträge nachbearbeiten +- Und der Admin hat jetzt eine Zeitzonen-Einstellungen in seinem Panel + +## Then … + + [Scene] + + A beach in the south sea. A straw hat on the left. + + Sully throws the hat-door open! Sully runs out the door, Mike is following. + + They frantically passing the picture leaving it to the right. + + +## Once upon a Time in the East + +- 2010-07-08 – going public with 1.0b1 +- 2010-06-21 – eating dogfoot +- 2010-06-17 – `git init .` for Saito + +## The Forgotten Founder + +- 2010 – RoR was finally abandoned, but valuable lessons were learned from Batu +- 2008 – Batu the Rails version was written diff --git a/docs/dev-hooks.md b/docs/dev-hooks.md index 123daff81..2e305c683 100644 --- a/docs/dev-hooks.md +++ b/docs/dev-hooks.md @@ -34,7 +34,7 @@ Data: - subject: View object -## Model.Saito.Posting.delete ## +## Model.Saito.Postings.delete ## Trigger: after a posting was deleted diff --git a/frontend/src/@types/index.d.ts b/frontend/src/@types/index.d.ts index a972d09c2..d60a20e8e 100644 --- a/frontend/src/@types/index.d.ts +++ b/frontend/src/@types/index.d.ts @@ -62,17 +62,6 @@ interface PNotify { } -declare module 'humanize' { - class humanize { - public date(size: string): string - public filesize(size: string): string - } - - const h: humanize; - - export default h; -} - declare module 'backbone.localstorage' { class LocalStorage { public constructor(key: string); @@ -92,7 +81,9 @@ declare module 'moment' { (date: string, formats: string[]): any (date: number[]): any + locale(locale: string): any unix(timestamp: number): any + } var moment: MomentStatic; diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index c2846c3b8..198f8f4f3 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -12,6 +12,11 @@ import ContentTimer from './ContentTimer'; import { Application } from 'backbone.marionette'; import 'lib/jquery.i18n/jquery.i18n.extend'; import 'lib/saito/backbone.initHelper'; +import moment from 'moment'; +/// Load numeral.js +import numeral from 'numeral'; +// load locales for numeral.js +require('numeral/locales') interface ISaitoCallbacks { beforeAppInit: CallableFunction[]; @@ -40,6 +45,9 @@ class Bootstrap { App.settings.set(SaitoApp.app.settings); + moment.locale(App.settings.get('language')); + numeral.locale(App.settings.get('language')); + $.ajax({ cache: true, dataType: 'json', diff --git a/frontend/src/locale/de.po b/frontend/src/locale/de.po index 54faf851b..df48c3633 100644 --- a/frontend/src/locale/de.po +++ b/frontend/src/locale/de.po @@ -18,7 +18,7 @@ msgstr "" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPathExcluded-0: *i18n*\n" -#: frontend/src/modules/answering/answering.ts:111 +#: frontend/src/modules/answering/answering.ts:91 msgid "answer.btn.preview" msgstr "Vorschau" @@ -26,7 +26,7 @@ msgstr "Vorschau" msgid "answer.btn.sbmt" msgstr "Eintragen" -#: frontend/src/modules/answering/views/CategorySelectVw.ts:22 +#: frontend/src/modules/answering/views/CategorySelectVw.ts:23 msgid "answer.cat.l" msgstr "Kategorie" @@ -34,36 +34,36 @@ msgstr "Kategorie" msgid "answer.cite.t" msgstr "Zitieren" -#: frontend/src/modules/answering/editor/DraftsView.ts:108 +#: frontend/src/modules/answering/Draft.ts:113 msgid "answer.draft.saved.t" msgstr "Antwort ist als Entwurf gespeichert." -#: frontend/src/modules/answering/editor/DraftsView.ts:109 +#: frontend/src/modules/answering/Draft.ts:117 msgid "answer.draft.unsaved.t" msgstr "Nicht alle Änderungen sind als Entwurf gespeichert." -#: frontend/src/modules/answering/answering.ts:96 +#: frontend/src/modules/answering/answering.ts:76 msgid "answer.reply.t" msgstr "Antwort verfassen" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:236 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:223 msgid "answer.subject.t" msgstr "Betreff" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:315 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:304 msgid "answer.submit.e.t" msgstr "Fehler: Beitrag wurde nicht gesichert" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:272 -#: frontend/src/modules/answering/answering.ts:314 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:261 +#: frontend/src/modules/answering/answering.ts:303 msgid "api.generic.e.exp" msgstr "Es konnte keine Verbindung zum Internet-Server hergestellt werden." -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:273 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:262 msgid "api.generic.e.t" msgstr "Fehler: Keine Verbindung zum Server" @@ -103,13 +103,13 @@ msgstr "Fehler: Lesezeichen konnte nicht gespeichert werden." msgid "bkm.title.pl" msgstr "Lesezeichen" -#: frontend/src/views/postingActionBookmark.js:13 -#: frontend/src/views/postingActionBookmark.js:65 +#: frontend/src/views/postingActionBookmark.ts:19 +#: frontend/src/views/postingActionBookmark.ts:70 msgid "bmk.doBookmark" msgstr "Eintrag zu den Lesezeichen hinzufügen" -#: frontend/src/views/postingActionBookmark.js:13 -#: frontend/src/views/postingActionBookmark.js:61 +#: frontend/src/views/postingActionBookmark.ts:19 +#: frontend/src/views/postingActionBookmark.ts:66 msgid "bmk.isBookmarked" msgstr "Eintrag ist in den Lesezeichen" @@ -151,32 +151,32 @@ msgstr "Einbinden" msgid "medins.notRecognized" msgstr "Nicht erkannt." -#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:9 -#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:57 +#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:17 +#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:65 msgid "medins.title" msgstr "Inhalte einbinden" -#: frontend/src/views/postingActionDelete.js:29 +#: frontend/src/views/postingActionDelete.ts:26 msgid "posting.delete.abort.btn" msgstr "Abbrechen" -#: frontend/src/views/postingActionDelete.js:31 -#: frontend/src/views/postingActionDelete.js:33 -#: frontend/src/views/postingActionDelete.js:54 +#: frontend/src/views/postingActionDelete.ts:28 +#: frontend/src/views/postingActionDelete.ts:30 +#: frontend/src/views/postingActionDelete.ts:42 msgid "posting.delete.title" msgstr "Beitrag löschen" -#: frontend/src/views/postingActionSolves.js:12 -#: frontend/src/views/postingActionSolves.js:28 +#: frontend/src/views/postingActionSolves.ts:22 +#: frontend/src/views/postingActionSolves.ts:33 msgid "posting.helpful" msgstr "Beitrag als hilfreich markieren." -#: frontend/src/modules/answering/views/PreviewVw.ts:45 -#: frontend/src/modules/answering/views/PreviewVw.ts:63 +#: frontend/src/modules/answering/views/PreviewVw.ts:43 +#: frontend/src/modules/answering/views/PreviewVw.ts:61 msgid "preview.e.generic" msgstr "Fehler: Vorschau konnte nicht geladen werden." -#: frontend/src/modules/answering/views/PreviewVw.ts:39 +#: frontend/src/modules/answering/views/PreviewVw.ts:37 msgid "preview.t" msgstr "Vorschau" @@ -185,25 +185,47 @@ msgstr "Vorschau" msgid "time.relative.yesterday" msgstr "Gestern" -#: frontend/src/views/postingActionDelete.js:24 +#: frontend/src/views/postingActionDelete.ts:21 msgid "tree.delete.confirm" msgstr "Sicher?" -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:24 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:41 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:53 +msgid "upl.add.calc" +msgstr "Berechne…" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:35 +msgid "upl.add.remaining.t" +msgstr "Verbleibend" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:39 +msgid "upl.add.size.t" +msgstr "Größe" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:31 +msgid "upl.add.speed.t" +msgstr "Geschwindigkeit" + +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:12 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:13 +msgid "upl.btn.abort.title" +msgstr "Abbrechen" + +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:25 msgid "upl.btn.insert" msgstr "Einfügen" -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:20 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:21 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:22 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:23 msgid "upl.btn.title" msgstr "Datei auswählen…" -#: frontend/src/modules/uploader/views/uploaderItemFooterVw.ts:26 +#: frontend/src/modules/uploader/views/uploaderItemFooterVw.ts:30 msgid "upl.del.btn" msgstr "Datei löschen" -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:8 -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:62 +#: frontend/src/modules/uploader/views/uploaderAddVw.ts:19 +#: frontend/src/modules/uploader/views/uploaderAddVw.ts:155 msgid "upl.failure" msgstr "Beim Hochladen ist ein Fehler aufgetreten." @@ -216,20 +238,19 @@ msgstr "Beim Hochladen ist ein Fehler aufgetreten." msgid "upl.loading" msgstr "Lade" -#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:10 -#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:24 +#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:11 +#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:27 msgid "upl.ncy" msgstr "Noch nichts hochgeladen." -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:8 -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:101 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:42 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:65 msgid "upl.new.title" msgstr "Datei hier hineinziehen" -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:26 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:7 -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:68 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:8 +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:27 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:39 +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:72 msgid "upl.title" msgstr "Hochladen" @@ -237,7 +258,17 @@ msgstr "Hochladen" msgid "upl.title.pl" msgstr "Uploads" -#: src/Template/Users/view.ctp:325 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:1 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:36 +msgid "upl.vald.e.dad" +msgstr "Datei wurde nicht erkannt." + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:1 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:40 +msgid "upl.vald.e.fileExists" +msgstr "Datei mit gleichen Namen wurde bereits hochgeladen." + +#: src/Template/Users/view.ctp:323 msgid "user.block.hours" msgstr "{hours} Stunden" diff --git a/frontend/src/locale/en.po b/frontend/src/locale/en.po index 48d344866..a89b896e2 100644 --- a/frontend/src/locale/en.po +++ b/frontend/src/locale/en.po @@ -18,7 +18,7 @@ msgstr "" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPathExcluded-0: *i18n*\n" -#: frontend/src/modules/answering/answering.ts:111 +#: frontend/src/modules/answering/answering.ts:91 msgid "answer.btn.preview" msgstr "Preview" @@ -26,7 +26,7 @@ msgstr "Preview" msgid "answer.btn.sbmt" msgstr "Submit" -#: frontend/src/modules/answering/views/CategorySelectVw.ts:22 +#: frontend/src/modules/answering/views/CategorySelectVw.ts:23 msgid "answer.cat.l" msgstr "Category" @@ -34,37 +34,37 @@ msgstr "Category" msgid "answer.cite.t" msgstr "Cite" -#: frontend/src/modules/answering/editor/DraftsView.ts:108 +#: frontend/src/modules/answering/Draft.ts:113 msgid "answer.draft.saved.t" msgstr "Answer is saved as draft." -#: frontend/src/modules/answering/editor/DraftsView.ts:109 +#: frontend/src/modules/answering/Draft.ts:117 msgid "answer.draft.unsaved.t" msgstr "Not all changes are saved as draft." -#: frontend/src/modules/answering/answering.ts:96 +#: frontend/src/modules/answering/answering.ts:76 msgid "answer.reply.t" msgstr "Write a Reply" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:236 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:223 msgid "answer.subject.t" msgstr "Subject" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:315 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:304 msgid "answer.submit.e.t" msgstr "Error: Posting Could Not Be Saved" -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:272 -#: frontend/src/modules/answering/answering.ts:314 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:261 +#: frontend/src/modules/answering/answering.ts:303 msgid "api.generic.e.exp" msgstr "" "Couldn't reach the internet server. Check your connection and try again." -#: frontend/src/modules/answering/answering.ts:129 -#: frontend/src/modules/answering/answering.ts:273 +#: frontend/src/modules/answering/answering.ts:109 +#: frontend/src/modules/answering/answering.ts:262 msgid "api.generic.e.t" msgstr "Error: Couldn't Connect to Server" @@ -104,13 +104,13 @@ msgstr "Error: Bookmark could not be saved." msgid "bkm.title.pl" msgstr "Bookmarks" -#: frontend/src/views/postingActionBookmark.js:13 -#: frontend/src/views/postingActionBookmark.js:65 +#: frontend/src/views/postingActionBookmark.ts:19 +#: frontend/src/views/postingActionBookmark.ts:70 msgid "bmk.doBookmark" msgstr "Bookmark the entry" -#: frontend/src/views/postingActionBookmark.js:13 -#: frontend/src/views/postingActionBookmark.js:61 +#: frontend/src/views/postingActionBookmark.ts:19 +#: frontend/src/views/postingActionBookmark.ts:66 msgid "bmk.isBookmarked" msgstr "Entry is bookmarked" @@ -152,32 +152,32 @@ msgstr "Insert" msgid "medins.notRecognized" msgstr "Nothing recognized." -#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:9 -#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:57 +#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:17 +#: frontend/src/modules/answering/editor/Menu/MediaInsertView.ts:65 msgid "medins.title" msgstr "Embed Content" -#: frontend/src/views/postingActionDelete.js:29 +#: frontend/src/views/postingActionDelete.ts:26 msgid "posting.delete.abort.btn" msgstr "Abort" -#: frontend/src/views/postingActionDelete.js:31 -#: frontend/src/views/postingActionDelete.js:33 -#: frontend/src/views/postingActionDelete.js:54 +#: frontend/src/views/postingActionDelete.ts:28 +#: frontend/src/views/postingActionDelete.ts:30 +#: frontend/src/views/postingActionDelete.ts:42 msgid "posting.delete.title" msgstr "Delete Posting" -#: frontend/src/views/postingActionSolves.js:12 -#: frontend/src/views/postingActionSolves.js:28 +#: frontend/src/views/postingActionSolves.ts:22 +#: frontend/src/views/postingActionSolves.ts:33 msgid "posting.helpful" msgstr "Mark entry as helpful." -#: frontend/src/modules/answering/views/PreviewVw.ts:45 -#: frontend/src/modules/answering/views/PreviewVw.ts:63 +#: frontend/src/modules/answering/views/PreviewVw.ts:43 +#: frontend/src/modules/answering/views/PreviewVw.ts:61 msgid "preview.e.generic" msgstr "Error: Couldn't load preview." -#: frontend/src/modules/answering/views/PreviewVw.ts:39 +#: frontend/src/modules/answering/views/PreviewVw.ts:37 msgid "preview.t" msgstr "Preview" @@ -186,25 +186,47 @@ msgstr "Preview" msgid "time.relative.yesterday" msgstr "yesterday" -#: frontend/src/views/postingActionDelete.js:24 +#: frontend/src/views/postingActionDelete.ts:21 msgid "tree.delete.confirm" msgstr "Are sure?" -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:24 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:41 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:53 +msgid "upl.add.calc" +msgstr "Calculating…" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:35 +msgid "upl.add.remaining.t" +msgstr "Remaining" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:39 +msgid "upl.add.size.t" +msgstr "Size" + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddStatsVw.ts:31 +msgid "upl.add.speed.t" +msgstr "Speed" + +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:12 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:13 +msgid "upl.btn.abort.title" +msgstr "Abort upload" + +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:25 msgid "upl.btn.insert" msgstr "Insert" -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:20 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:21 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:22 +#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:23 msgid "upl.btn.title" msgstr "Chose file…" -#: frontend/src/modules/uploader/views/uploaderItemFooterVw.ts:26 +#: frontend/src/modules/uploader/views/uploaderItemFooterVw.ts:30 msgid "upl.del.btn" msgstr "Delete file" -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:8 -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:62 +#: frontend/src/modules/uploader/views/uploaderAddVw.ts:19 +#: frontend/src/modules/uploader/views/uploaderAddVw.ts:155 msgid "upl.failure" msgstr "Error occurred on upload." @@ -217,20 +239,19 @@ msgstr "Error occurred on upload." msgid "upl.loading" msgstr "Loading" -#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:10 -#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:24 +#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:11 +#: frontend/src/modules/uploader/views/uploaderCollectionVw.ts:27 msgid "upl.ncy" msgstr "No uploads yet." -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:8 -#: frontend/src/modules/uploader/views/uploaderAddVw.ts:101 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:42 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:65 msgid "upl.new.title" msgstr "Drop File Here" -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:26 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:7 -#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:68 -#: frontend/src/modules/uploader/templates/uploaderAddTpl.html:8 +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:27 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddDragAreaVw.ts:39 +#: frontend/src/modules/answering/editor/MenuButton/MenuButtonUploadView.ts:72 msgid "upl.title" msgstr "Uploads" @@ -238,7 +259,17 @@ msgstr "Uploads" msgid "upl.title.pl" msgstr "Uploads" -#: src/Template/Users/view.ctp:325 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:1 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:36 +msgid "upl.vald.e.dad" +msgstr "Didn't find a file." + +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:1 +#: frontend/src/modules/uploader/views/uploaderAdd/uploaderAddMdl.ts:40 +msgid "upl.vald.e.fileExists" +msgstr "File with same name already uploaded." + +#: src/Template/Users/view.ctp:323 msgid "user.block.hours" msgstr "{hours} hours" diff --git a/frontend/src/modules/answering/answering.ts b/frontend/src/modules/answering/answering.ts index 58eedbc8c..e48e0e5f9 100644 --- a/frontend/src/modules/answering/answering.ts +++ b/frontend/src/modules/answering/answering.ts @@ -249,6 +249,9 @@ export default class AnsweringView extends View { this.triggerMethod('answering:form:rendered', data); App.eventBus.trigger('change:DOM'); + + // Uploader debug + // this.$('.btn-markup-Upload').click(); } /** diff --git a/frontend/src/modules/modalDialog/modalDialog.ts b/frontend/src/modules/modalDialog/modalDialog.ts index 72b4644b4..fe5a0351d 100644 --- a/frontend/src/modules/modalDialog/modalDialog.ts +++ b/frontend/src/modules/modalDialog/modalDialog.ts @@ -47,6 +47,9 @@ class ModalDialogView extends View { App.eventBus.trigger('app:modal:shown'); this.triggerMethod('shown'); }); + this.$el.parent().on('hidden.bs.modal', () => { + this.triggerMethod('hidden'); + }); this.$el.parent().modal('show'); } @@ -54,6 +57,10 @@ class ModalDialogView extends View { this.$el.parent().modal('hide'); } + public onHidden() { + this.getRegion('content').empty(); + } + public invalidInput() { this.$el.addClass('animation shake'); _.delay(() => { diff --git a/frontend/src/modules/notification/notification.ts b/frontend/src/modules/notification/notification.ts index 67727c6ee..c2bcdf6f2 100644 --- a/frontend/src/modules/notification/notification.ts +++ b/frontend/src/modules/notification/notification.ts @@ -152,7 +152,6 @@ export default class NotificationsView extends Mn.View { break; case 'warning': logOptions.addclass = 'bg-warning'; - logOptions.autohide = false; break; case 'error': logOptions.addclass = 'bg-danger'; diff --git a/frontend/src/modules/uploader/templates/uploaderAddTpl.html b/frontend/src/modules/uploader/templates/uploaderAddTpl.html index 0315d4852..9c8c16edb 100644 --- a/frontend/src/modules/uploader/templates/uploaderAddTpl.html +++ b/frontend/src/modules/uploader/templates/uploaderAddTpl.html @@ -1,30 +1,33 @@
-
-
-
- -
-

- <%- $.i18n.__('upl.title') %> -

-
+
+
+
+
+
+
diff --git a/plugins/SaitoSearch/src/Model/Behavior/SaitoSearchBehavior.php b/plugins/SaitoSearch/src/Model/Behavior/SaitoSearchBehavior.php index 4cd4e295e..9eeb6c47e 100644 --- a/plugins/SaitoSearch/src/Model/Behavior/SaitoSearchBehavior.php +++ b/plugins/SaitoSearch/src/Model/Behavior/SaitoSearchBehavior.php @@ -64,7 +64,7 @@ public function findSimpleSearchByRank(Query $query, array $options): Query 'relSubject' => 'MATCH (Entries.subject) AGAINST (:q IN BOOLEAN MODE)', 'relText' => 'MATCH (Entries.text) AGAINST (:q IN BOOLEAN MODE)', 'relName' => 'MATCH (Entries.name) AGAINST (:q IN BOOLEAN MODE)' - ] + $table->threadLineFieldList + ] + $table->getFieldset() ) ->where("MATCH (`Entries`.`subject`, `Entries`.`text`, `Entries`.`name`) AGAINST (:q IN BOOLEAN MODE)") ->order(['(2*relSubject + relText + 4*relName)' => 'DESC', '`Entries`.`time`' => 'DESC']) diff --git a/plugins/SaitoSearch/src/Template/Element/search_results.ctp b/plugins/SaitoSearch/src/Template/Element/search_results.ctp index 385dc9f89..ee31d4496 100644 --- a/plugins/SaitoSearch/src/Template/Element/search_results.ctp +++ b/plugins/SaitoSearch/src/Template/Element/search_results.ctp @@ -10,7 +10,7 @@ ); } else { foreach ($results as $result) { - echo $this->Posting->renderThread($result->toPosting(), ['rootWrap' => true]); + echo $this->Posting->renderThread($result->toPosting()->withCurrentUser($CurrentUser), ['rootWrap' => true]); } } ?> diff --git a/plugins/Stopwatch/src/Lib/Stopwatch.php b/plugins/Stopwatch/src/Lib/Stopwatch.php index 1c41ef629..d8416df4d 100644 --- a/plugins/Stopwatch/src/Lib/Stopwatch.php +++ b/plugins/Stopwatch/src/Lib/Stopwatch.php @@ -110,7 +110,7 @@ protected static function _addEvent($x, $event = null) // phpcs:disable Generic.PHP.NoSilencedErrors.Discouraged $dat = @getrusage(); // phpcs:enable Generic.PHP.NoSilencedErrors.Discouraged - if ($dat === null) { + if (empty($dat)) { // some hosters disable getrusage() while hardening their PHP $utime = 0; } else { diff --git a/src/Auth/AuthenticationServiceFactory.php b/src/Auth/AuthenticationServiceFactory.php index 3d60167f5..fbd751b38 100644 --- a/src/Auth/AuthenticationServiceFactory.php +++ b/src/Auth/AuthenticationServiceFactory.php @@ -51,7 +51,7 @@ public static function buildApp(): AuthenticationService $service = new AuthenticationService(); $service->setConfig('queryParam', 'redirect'); - $service->setConfig('unauthenticatedRedirect', '/login'); + $service->setConfig('unauthenticatedRedirect', Router::url(['_name' => 'login'], false)); $service->loadIdentifier('Authentication.Password', [ 'passwordHasher' => [ diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index 786bd5d35..1e058c8c8 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -61,6 +61,7 @@ class AppController extends Controller 'Html', 'JsData', 'Layout', + 'Permissions', 'SaitoHelp.SaitoHelp', 'Stopwatch.Stopwatch', 'TimeH', @@ -124,7 +125,6 @@ public function beforeFilter(Event $event) { Stopwatch::start('App->beforeFilter()'); - $this->Themes->set($this->CurrentUser); // disable forum with admin pref if (Configure::read('Saito.Settings.forum_disabled') && $this->request->getParam('action') !== 'login' && @@ -138,15 +138,11 @@ public function beforeFilter(Event $event) return null; } - $this->_setConfigurationFromGetParams(); - // allow sql explain for DebugKit toolbar if ($this->request->getParam('plugin') === 'debug_kit') { $this->Authentication->allowUnauthenticated(['sql_explain']); } - $this->_l10nRenderFile(); - Stopwatch::stop('App->beforeFilter()'); } @@ -156,6 +152,10 @@ public function beforeFilter(Event $event) public function beforeRender(Event $event) { Stopwatch::start('App->beforeRender()'); + $this->Themes->set($this->CurrentUser); + $this->_setConfigurationFromGetParams(); + $this->_l10nRenderFile(); + $this->set('SaitoSettings', new SettingsImmutable(Configure::read('Saito.Settings'))); $this->set('SaitoEventManager', SaitoEventManager::getInstance()); $this->set('showStopwatch', $this->getConfig('showStopwatch')); diff --git a/src/Controller/Component/AuthUserComponent.php b/src/Controller/Component/AuthUserComponent.php index ba5134b3b..06d569053 100755 --- a/src/Controller/Component/AuthUserComponent.php +++ b/src/Controller/Component/AuthUserComponent.php @@ -25,7 +25,7 @@ use Cake\ORM\TableRegistry; use DateTimeImmutable; use Firebase\JWT\JWT; -use Saito\App\Registry; +use Saito\RememberTrait; use Saito\User\Cookie\Storage; use Saito\User\CurrentUser\CurrentUser; use Saito\User\CurrentUser\CurrentUserFactory; @@ -39,6 +39,8 @@ */ class AuthUserComponent extends Component { + use RememberTrait; + /** * Component name * @@ -69,6 +71,13 @@ class AuthUserComponent extends Component */ protected $UsersTable = null; + /** + * Array of authorized actions 'action' => 'resource' + * + * @var array + */ + private $actionAuthorizationResources = []; + /** * {@inheritDoc} */ @@ -102,11 +111,17 @@ public function initialize(array $config) $this->setCurrentUser($CurrentUser); + Stopwatch::stop('CurrentUser::initialize()'); + } + + /** + * {@inheritDoc} + */ + public function startup() + { if (!$this->isAuthorized($this->CurrentUser)) { - throw new ForbiddenException(); + throw new ForbiddenException(null, 1571852880); } - - Stopwatch::stop('CurrentUser::initialize()'); } /** @@ -116,7 +131,7 @@ public function initialize(array $config) */ public function isBot() { - return $this->request->is('bot'); + return $this->remember('isBot', $this->getController()->getRequest()->is('bot')); } /** @@ -243,7 +258,10 @@ private function refreshAuthenticationProvider() } $expire = $authenticationProvider->getConfig('cookie.expire'); - $refreshedCookie = $cookie->withExpiry($expire); + $refreshedCookie = $cookie + ->withExpiry($expire) + // Can't read path from cookies, so the default would be root '/'. + ->withPath($this->getController()->getRequest()->getAttribute('webroot')); $response = $controller->getResponse()->withCookie($refreshedCookie); $controller->setResponse($response); @@ -334,7 +352,18 @@ private function setCurrentUser(CurrentUserInterface $CurrentUser): void $controller->CurrentUser = $this->CurrentUser; // makes CurrentUser available as View var in templates $controller->set('CurrentUser', $this->CurrentUser); - Registry::set('CU', $this->CurrentUser); + } + + /** + * The controller action will be authorized with a permission resource. + * + * @param string $action The controller action to authorize. + * @param string $resource The permission resource token. + * @return void + */ + public function authorizeAction(string $action, string $resource) + { + $this->actionAuthorizationResources[$action] = $resource; } /** @@ -345,17 +374,13 @@ private function setCurrentUser(CurrentUserInterface $CurrentUser): void */ private function isAuthorized(CurrentUser $user) { - $controller = $this->getController(); - $action = $controller->getRequest()->getParam('action'); - - if (isset($controller->actionAuthConfig) - && isset($controller->actionAuthConfig[$action])) { - $requiredRole = $controller->actionAuthConfig[$action]; - - return Registry::get('Permission') - ->check($user->getRole(), $requiredRole); + /// Authorize action through resource + $action = $this->getController()->getRequest()->getParam('action'); + if (isset($this->actionAuthorizationResources[$action])) { + return $user->permission($this->actionAuthorizationResources[$action]); } + /// Authorize admin area $prefix = $this->request->getParam('prefix'); $plugin = $this->request->getParam('plugin'); $isAdminRoute = ($prefix && strtolower($prefix) === 'admin') diff --git a/src/Controller/Component/MarkAsReadComponent.php b/src/Controller/Component/MarkAsReadComponent.php index d26ad6bef..5bf94b53c 100644 --- a/src/Controller/Component/MarkAsReadComponent.php +++ b/src/Controller/Component/MarkAsReadComponent.php @@ -14,9 +14,7 @@ use App\Controller\AppController; use Cake\Controller\Component; -use Saito\App\Registry; use Saito\Posting\PostingInterface; -use Saito\User\CurrentUser\CurrentUserInterface; /** * Class MarkAsReadComponent @@ -51,7 +49,9 @@ public function shutdown() */ public function next() { - $CU = Registry::get('CU'); + /** @var AppController */ + $controller = $this->getController(); + $CU = $controller->CurrentUser; if (!$CU->isLoggedIn() || !$CU->get('user_automaticaly_mark_as_read')) { return; } @@ -68,8 +68,9 @@ public function next() */ public function refresh(array $options = []) { - /** @var CurrentUserInterface */ - $CU = Registry::get('CU'); + /** @var AppController */ + $controller = $this->getController(); + $CU = $controller->CurrentUser; if ($this->request->is('preview') || !$CU->isLoggedIn()) { return false; } @@ -90,7 +91,7 @@ public function refresh(array $options = []) $session->write('User.last_refresh_tmp', $lastRefreshTemp); } - if ($this->request->getQuery('mar', false) !== false) { + if ($this->request->getQuery('mar') !== null) { // a second session A shall not accidentally mark something as read that isn't read on session B if ($lastRefreshTemp > $CU->get('last_refresh_unix')) { $CU->getLastRefresh()->set(); diff --git a/src/Controller/Component/ThreadsComponent.php b/src/Controller/Component/ThreadsComponent.php index 9e2ae1252..fd7b5409c 100644 --- a/src/Controller/Component/ThreadsComponent.php +++ b/src/Controller/Component/ThreadsComponent.php @@ -17,9 +17,7 @@ use Cake\Controller\Component\PaginatorComponent; use Cake\Core\Configure; use Cake\ORM\Entity; -use Cake\ORM\TableRegistry; -use Saito\App\Registry; -use Saito\Posting\Posting; +use Saito\Posting\Basic\BasicPostingInterface; use Saito\User\CurrentUser\CurrentUserInterface; use Stopwatch\Lib\Stopwatch; @@ -31,7 +29,6 @@ */ class ThreadsComponent extends Component { - public $components = ['AuthUser', 'Paginator']; /** @@ -39,35 +36,42 @@ class ThreadsComponent extends Component * * @var EntriesTable */ - private $Entries; + protected $Table; + + /** + * {@inheritDoc} + */ + public function initialize(array $config) + { + parent::initialize($config); + $this->Table = $config['table']; + } /** * Load paginated threads * * @param mixed $order order to apply + * @param CurrentUserInterface $CurrentUser CurrentUser * @return array */ - public function paginate($order) + public function paginate($order, CurrentUserInterface $CurrentUser): array { - /** @var EntriesTable */ - $EntriesTable = TableRegistry::getTableLocator()->get('Entries'); - $this->Entries = $EntriesTable; - - $CurrentUser = $this->_getCurrentUser(); - $initials = $this->_getInitialThreads($CurrentUser, $order); - $threads = $this->Entries->treesForThreads($initials, $order); + $initials = $this->paginateThreads($order, $CurrentUser); + if (empty($initials)) { + return []; + } - return $threads; + return $this->Table->postingsForThreads($initials, $order, $CurrentUser); } /** * Gets thread ids for paginated entries/index. * - * @param CurrentUserInterface $User current-user * @param array $order sort order + * @param CurrentUserInterface $User current-user * @return array thread ids */ - protected function _getInitialThreads(CurrentUserInterface $User, $order) + protected function paginateThreads($order, CurrentUserInterface $User): array { Stopwatch::start('Entries->_getInitialThreads() Paginate'); $categories = $User->getCategories()->getCurrent('read'); @@ -87,7 +91,7 @@ protected function _getInitialThreads(CurrentUserInterface $User, $order) // Performance: Custom counter from categories counter-cache; // avoids a costly COUNT(*) DB call counting all pages for pagination. 'counter' => function ($query) use ($categories) { - $results = $this->Entries->Categories->find('all') + $results = $this->Table->Categories->find('all') ->select(['thread_count']) ->where(['id IN' => $categories]) ->all(); @@ -109,7 +113,7 @@ function ($carry, Entity $entity) { // use setConfig on Component to not merge but overwrite/set the config $this->Paginator->setConfig('whitelist', ['page'], false); - $initialThreads = $this->Paginator->paginate($this->Entries, $settings); + $initialThreads = $this->Paginator->paginate($this->Table, $settings); $initialThreadsNew = []; foreach ($initialThreads as $k => $v) { @@ -121,32 +125,36 @@ function ($carry, Entity $entity) { } /** - * Increment views for posting if posting doesn't belong to current user. - * - * @param Posting $posting posting - * @param string $type type - * - 'null' increment single posting - * - 'thread' increment all postings in thread + * Increment views for all postings in thread * + * @param BasicPostingInterface $posting posting + * @param CurrentUserInterface $CurrentUser current user * @return void */ - public function incrementViews(Posting $posting, $type = null) + public function incrementViewsForThread(BasicPostingInterface $posting, CurrentUserInterface $CurrentUser) { if ($this->AuthUser->isBot()) { return; } - /** @var EntriesTable */ - $Entries = TableRegistry::getTableLocator()->get('Entries'); - $CurrentUser = $this->_getCurrentUser(); + $where = ['tid' => $posting->get('tid')]; + if ($CurrentUser->isLoggedIn()) { + $where['user_id !='] = $CurrentUser->getId(); + } - if ($type === 'thread') { - $where = ['tid' => $posting->get('tid')]; - if ($CurrentUser->isLoggedIn()) { - $where['user_id !='] = $CurrentUser->getId(); - } - $Entries->increment($where, 'views'); + $this->Table->increment($where, 'views'); + } + /** + * Increment views for posting if posting + * + * @param BasicPostingInterface $posting posting + * @param CurrentUserInterface $CurrentUser current user + * @return void + */ + public function incrementViewsForPosting(BasicPostingInterface $posting, CurrentUserInterface $CurrentUser) + { + if ($this->AuthUser->isBot()) { return; } @@ -155,19 +163,6 @@ public function incrementViews(Posting $posting, $type = null) return; } - $Entries->increment($posting->get('id'), 'views'); - } - - /** - * Get CurrentUser - * - * @return CurrentUserInterface - */ - protected function _getCurrentUser(): CurrentUserInterface - { - /** @var CurrentUserInterface */ - $CU = Registry::get('CU'); - - return $CU; + $this->Table->increment($posting->get('id'), 'views'); } } diff --git a/src/Controller/EntriesController.php b/src/Controller/EntriesController.php index 6df329851..808ab6207 100644 --- a/src/Controller/EntriesController.php +++ b/src/Controller/EntriesController.php @@ -18,15 +18,16 @@ use App\Controller\Component\ThreadsComponent; use App\Model\Table\EntriesTable; use Cake\Core\Configure; +use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\Event; use Cake\Http\Exception\BadRequestException; +use Cake\Http\Exception\ForbiddenException; use Cake\Http\Exception\MethodNotAllowedException; use Cake\Http\Exception\NotFoundException; use Cake\Http\Response; use Cake\Routing\RequestActionTrait; use Saito\Exception\SaitoForbiddenException; -use Saito\Posting\Posting; -use Saito\Posting\PostingInterface; +use Saito\Posting\Basic\BasicPostingInterface; use Saito\User\CurrentUser\CurrentUserInterface; use Stopwatch\Lib\Stopwatch; @@ -45,12 +46,6 @@ class EntriesController extends AppController public $helpers = ['Posting', 'Text']; - public $actionAuthConfig = [ - 'ajaxToggle' => 'mod', - 'merge' => 'mod', - 'delete' => 'mod' - ]; - /** * {@inheritDoc} */ @@ -60,7 +55,7 @@ public function initialize() $this->loadComponent('MarkAsRead'); $this->loadComponent('Referer'); - $this->loadComponent('Threads'); + $this->loadComponent('Threads', ['table' => $this->Entries]); } /** @@ -80,7 +75,7 @@ public function index() $order = ['fixed' => 'DESC', $sortKey => 'DESC']; //= get threads - $threads = $this->Threads->paginate($order); + $threads = $this->Threads->paginate($order, $this->CurrentUser); $this->set('entries', $threads); $currentPage = (int)$this->request->getQuery('page') ?: 1; @@ -124,21 +119,13 @@ public function mix($tid) throw new BadRequestException(); } - $postings = $this->Entries->treeForNode( - $tid, - ['root' => true, 'complete' => true] - ); + try { + $postings = $this->Entries->postingsForThread($tid, true, $this->CurrentUser); + } catch (RecordNotFoundException $e) { + /// redirect sub-posting to mix view of thread + $actualTid = $this->Entries->getThreadId($tid); - /// redirect sub-posting to mix view of thread - if (!$postings) { - $post = $this->Entries->find() - ->select(['tid']) - ->where(['id' => $tid]) - ->first(); - if (!empty($post)) { - return $this->redirect([$post->get('tid'), '#' => $tid], 301); - } - throw new NotFoundException; + return $this->redirect([$actualTid, '#' => $tid], 301); } // check if anonymous tries to access internal categories @@ -155,7 +142,7 @@ public function mix($tid) $this->_showAnsweringPanel(); - $this->Threads->incrementViews($root, 'thread'); + $this->Threads->incrementViewsForThread($root, $this->CurrentUser); $this->MarkAsRead->thread($postings); } @@ -209,16 +196,18 @@ public function view($id = null) return $this->redirect('/'); } - if (!$this->CurrentUser->getCategories()->permission('read', $entry->get('category'))) { + $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser); + + if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) { return $this->_requireAuth(); } - $this->set('entry', $entry); - $this->Threads->incrementViews($entry); - $this->_setRootEntry($entry); + $this->set('entry', $posting); + $this->Threads->incrementViewsForPosting($posting, $this->CurrentUser); + $this->_setRootEntry($posting); $this->_showAnsweringPanel(); - $this->MarkAsRead->posting($entry); + $this->MarkAsRead->posting($posting); // inline open if ($this->request->is('ajax')) { @@ -228,9 +217,9 @@ public function view($id = null) // full page request $this->set( 'tree', - $this->Entries->treeForNode($entry->get('tid'), ['root' => true]) + $this->Entries->postingsForThread($posting->get('tid'), false, $this->CurrentUser) ); - $this->Title->setFromPosting($entry); + $this->Title->setFromPosting($posting); Stopwatch::stop('Entries->view()'); } @@ -260,11 +249,11 @@ public function edit($id = null) throw new BadRequestException; } - /** @var PostingInterface */ - $posting = $this->Entries->get($id); - if (!$posting) { + $entry = $this->Entries->get($id); + if (empty($entry)) { throw new NotFoundException; } + $posting = $entry->toPosting()->withCurrentUser($this->CurrentUser); if (!$posting->isEditingAllowed()) { throw new SaitoForbiddenException( @@ -301,7 +290,7 @@ public function edit($id = null) */ public function threadLine($id = null) { - $posting = $this->Entries->get($id); + $posting = $this->Entries->get($id)->toPosting()->withCurrentUser($this->CurrentUser); if (!$this->CurrentUser->getCategories()->permission('read', $posting->get('category'))) { return $this->_requireAuth(); } @@ -320,9 +309,10 @@ public function threadLine($id = null) * @throws NotFoundException * @throws MethodNotAllowedException */ - public function delete($id = null) + public function delete(string $id) { //$this->request->allowMethod(['post', 'delete']); + $id = (int)$id; if (!$id) { throw new NotFoundException; } @@ -332,7 +322,14 @@ public function delete($id = null) throw new NotFoundException; } - $success = $this->Entries->treeDeleteNode($id); + $action = $posting->isRoot() ? 'thread' : 'answer'; + $allowed = $this->CurrentUser->getCategories() + ->permission($action, $posting->get('category_id')); + if (!$allowed) { + throw new ForbiddenException(null, 1571309481); + } + + $success = $this->Entries->deletePosting($id); if ($success) { $flashType = 'success'; @@ -374,7 +371,7 @@ public function solve($id) { $this->autoRender = false; try { - $posting = $this->Entries->get($id, ['return' => 'Entity']); + $posting = $this->Entries->get($id); if (empty($posting)) { throw new \InvalidArgumentException('Posting to mark solved not found.'); @@ -418,9 +415,9 @@ public function merge($sourceId = null) } /* @var Entry */ - $posting = $this->Entries->findById($sourceId)->first(); + $entry = $this->Entries->findById($sourceId)->first(); - if (!$posting || !$posting->isRoot()) { + if (!$entry || !$entry->isRoot()) { throw new NotFoundException(); } @@ -437,7 +434,7 @@ public function merge($sourceId = null) } $this->viewBuilder()->setLayout('Admin.admin'); - $this->set(compact('posting')); + $this->set('posting', $entry); } /** @@ -486,6 +483,10 @@ public function beforeFilter(Event $event) ); $this->Authentication->allowUnauthenticated(['index', 'view', 'mix', 'update']); + $this->AuthUser->authorizeAction('ajaxToggle', 'saito.core.posting.pinAndLock'); + $this->AuthUser->authorizeAction('merge', 'saito.core.posting.merge'); + $this->AuthUser->authorizeAction('delete', 'saito.core.posting.delete'); + Stopwatch::stop('Entries->beforeFilter()'); } @@ -564,10 +565,10 @@ protected function _showAnsweringPanel() /** * makes root posting of $posting avaiable in view * - * @param Posting $posting posting for root entry + * @param BasicPostingInterface $posting posting for root entry * @return void */ - protected function _setRootEntry(Posting $posting) + protected function _setRootEntry(BasicPostingInterface $posting) { if (!$posting->isRoot()) { $root = $this->Entries->find() diff --git a/src/Controller/PostingsController.php b/src/Controller/PostingsController.php index fef3f1b79..7481bedfb 100644 --- a/src/Controller/PostingsController.php +++ b/src/Controller/PostingsController.php @@ -80,7 +80,7 @@ public function edit(string $id): void } $id = $data['id']; - $posting = $this->Entries->get($id, ['return' => 'Entity']); + $posting = $this->Entries->get($id); if (!$posting) { throw new NotFoundException('Posting not found.'); } @@ -118,7 +118,7 @@ public function meta(?string $id = null): void if ($isAnswer) { /** @var PostingInterface */ - $parent = $this->Entries->get($pid); + $parent = $this->Entries->get($pid)->toPosting()->withCurrentUser($this->CurrentUser); // Don't leak content of forbidden categories if ($parent->isAnsweringForbidden()) { @@ -133,7 +133,7 @@ public function meta(?string $id = null): void if ($isEdit) { /** @var PostingInterface */ - $posting = $this->Entries->get($id); + $posting = $this->Entries->get($id)->toPosting()->withCurrentUser($this->CurrentUser); if (!$posting->isEditingAllowed()) { throw new SaitoForbiddenException( 'Access to posting in PostingsController:meta() forbidden.', diff --git a/src/Controller/PreviewController.php b/src/Controller/PreviewController.php index 4e4a6c8e1..d0d620b32 100644 --- a/src/Controller/PreviewController.php +++ b/src/Controller/PreviewController.php @@ -16,7 +16,6 @@ use App\Model\Table\EntriesTable; use Cake\I18n\Time; use Cake\View\Helper\IdGeneratorTrait; -use Saito\App\Registry; /** * Class EntriesController @@ -58,6 +57,7 @@ public function preview() $data = $this->Entries->prepareChildPosting($parent, $data); } + /** @var \App\Model\Entity\Entry */ $newEntry = $this->Entries->newEntity($data); $errors = $newEntry->getErrors(); @@ -67,10 +67,7 @@ public function preview() $newEntry['category'] = $this->Entries->Categories->find() ->where(['id' => $newEntry['category_id']]) ->first(); - $posting = Registry::newInstance( - '\Saito\Posting\Posting', - ['rawData' => $newEntry->toArray()] - ); + $posting = $newEntry->toPosting()->withCurrentUser($this->CurrentUser); $this->set(compact('posting')); } else { $this->set(compact('errors')); diff --git a/src/Controller/UsersController.php b/src/Controller/UsersController.php index d5681d68e..d9e5686bc 100644 --- a/src/Controller/UsersController.php +++ b/src/Controller/UsersController.php @@ -17,15 +17,17 @@ use Cake\Core\Configure; use Cake\Event\Event; use Cake\Http\Exception\BadRequestException; -use Cake\Http\Exception\NotFoundException; +use Cake\Http\Exception\ForbiddenException; use Cake\Http\Response; use Cake\I18n\Time; use Cake\Routing\Router; +use Saito\App\Registry; use Saito\Exception\Logger\ExceptionLogger; use Saito\Exception\Logger\ForbiddenLogger; use Saito\Exception\SaitoForbiddenException; use Saito\User\Blocker\ManualBlocker; -use Saito\User\CurrentUser\CurrentUserInterface; +use Saito\User\Permission\Permissions; +use Saito\User\Permission\ResourceAI; use Siezi\SimpleCaptcha\Model\Validation\SimpleCaptchaValidator; use Stopwatch\Lib\Stopwatch; @@ -41,13 +43,6 @@ class UsersController extends AppController 'Text' ]; - /** - * Are moderators allowed to bloack users - * - * @var bool - */ - protected $modLocking = false; - /** * {@inheritDoc} */ @@ -85,10 +80,13 @@ public function login() if ($this->AuthUser->login()) { // Redirect query-param in URL. $target = $this->getRequest()->getQuery('redirect'); + // AuthenticationService puts the full local path into the redirect + // parameter, so we have to strip the base-path off again. + $target = Router::normalize($target); // Referer from Request $target = $target ?: $this->referer(null, true); - if (!$target || $this->Referer->wasAction('login')) { + if (empty($target)) { $target = '/'; } @@ -98,15 +96,13 @@ public function login() /// error on login $username = $this->request->getData('username'); /** @var User */ - $readUser = $this->Users->find() + $User = $this->Users->find() ->where(['username' => $username]) ->first(); $message = __('user.authe.e.generic'); - if (!empty($readUser)) { - $User = $readUser->toSaitoUser(); - + if (!empty($User)) { if (!$User->isActivated()) { $message = __('user.actv.ny'); } elseif ($User->isLocked()) { @@ -278,7 +274,7 @@ public function index() ], 'registered' => [__('registered'), ['direction' => 'desc']] ]; - $showBlocked = Configure::read('Saito.Settings.block_user_ui'); + $showBlocked = $this->CurrentUser->permission('saito.core.user.lock.view'); if ($showBlocked) { $menuItems['user_lock'] = [ __('user.set.lock.t'), @@ -418,7 +414,7 @@ public function view($id = null) $entriesShownOnPage = 20; $this->set( 'lastEntries', - $this->Users->Entries->getRecentEntries( + $this->Users->Entries->getRecentPostings( $this->CurrentUser, ['user_id' => $id, 'limit' => $entriesShownOnPage] ) @@ -434,8 +430,6 @@ public function view($id = null) $user->set('ignores', $ignores); } - $isEditingAllowed = $this->_isEditingAllowed($this->CurrentUser, $id); - $blockForm = new BlockForm(); $solved = $this->Users->countSolved($id); $this->set(compact('blockForm', 'isEditingAllowed', 'solved', 'user')); @@ -450,7 +444,24 @@ public function view($id = null) */ public function avatar($userId) { - $data = []; + if (!$this->Users->exists($userId)) { + throw new BadRequestException; + } + + /** @var User */ + $user = $this->Users->get($userId); + + $permissionEditing = $this->CurrentUser->permission( + 'saito.core.user.edit', + (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId()) + ); + if (!$permissionEditing) { + throw new \Saito\Exception\SaitoForbiddenException( + "Attempt to edit user $userId.", + ['CurrentUser' => $this->CurrentUser] + ); + } + if ($this->request->is('post') || $this->request->is('put')) { $data = [ 'avatar' => $this->request->getData('avatar'), @@ -462,11 +473,19 @@ public function avatar($userId) 'avatar_dir' => null ]; } + $patched = $this->Users->patchEntity($user, $data); + $errors = $patched->getErrors(); + if (empty($errors) && $this->Users->save($patched)) { + return $this->redirect(['action' => 'edit', $userId]); + } else { + $this->Flash->set( + __('The user could not be saved. Please, try again.'), + ['element' => 'error'] + ); + } } - $user = $this->_edit($userId, $data); - if ($user === true) { - return $this->redirect(['action' => 'edit', $userId]); - } + + $this->set('user', $user); $this->set( 'titleForPage', @@ -483,22 +502,35 @@ public function avatar($userId) */ public function edit($id = null) { - $data = []; + /** @var User */ + $user = $this->Users->get($id); + + $permissionEditing = $this->CurrentUser->permission( + 'saito.core.user.edit', + (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId()) + ); + if (!$permissionEditing) { + throw new \Saito\Exception\SaitoForbiddenException( + sprintf('Attempt to edit user "%s".', $user->get('id')), + ['CurrentUser' => $this->CurrentUser] + ); + } + if ($this->request->is('post') || $this->request->is('put')) { $data = $this->request->getData(); - unset($data['id']); - //= make sure only admin can edit these fields - if (!$this->CurrentUser->permission('saito.core.user.edit')) { - // @td DRY: refactor this admin fields together with view - unset($data['username'], $data['user_email'], $data['user_type']); + $patched = $this->Users->patchEntity($user, $data); + $errors = $patched->getErrors(); + if (empty($errors) && $this->Users->save($patched)) { + return $this->redirect(['action' => 'view', $id]); } - } - $user = $this->_edit($id, $data); - if ($user === true) { - return $this->redirect(['action' => 'view', $id]); - } + $this->Flash->set( + __('The user could not be saved. Please, try again.'), + ['element' => 'error'] + ); + } $this->set('user', $user); + $this->set( 'titleForPage', __('user.edit.t', [$user->get('username')]) @@ -511,43 +543,69 @@ public function edit($id = null) } /** - * Handle user edit core. Retrieve user or patch if data is passed. - * - * @param string $userId user-ID - * @param array|null $data datat to update the user + * delete user * - * @return true|User true on successful save, patched user otherwise + * @param string $id user-ID + * @return \Cake\Network\Response|void */ - protected function _edit($userId, array $data = null) + public function delete($id) { - if (!$this->_isEditingAllowed($this->CurrentUser, $userId)) { - throw new \Saito\Exception\SaitoForbiddenException( - "Attempt to edit user $userId.", - ['CurrentUser' => $this->CurrentUser] + $id = (int)$id; + /** @var User */ + $readUser = $this->Users->get($id); + + /// Check permission + $permission = $this->CurrentUser->permission( + 'saito.core.user.delete', + (new ResourceAI())->onRole($readUser->getRole()) + ); + if (!$permission) { + throw new ForbiddenException( + sprintf( + 'User "%s" is not allowed to delete user "%s".', + $this->CurrentUser->get('username'), + $readUser->get('username') + ), + 1571811593 ); } - if (!$this->Users->exists($userId)) { - throw new BadRequestException; + + $this->set('user', $readUser); + + $failure = false; + if (!$this->request->getData('userdeleteconfirm')) { + $failure = true; + $this->Flash->set(__('user.del.fail.3'), ['element' => 'error']); + } elseif ($this->CurrentUser->isUser($readUser)) { + $failure = true; + $this->Flash->set(__('user.del.fail.1'), ['element' => 'error']); } - /** @var User */ - $user = $this->Users->get($userId); - if ($data) { - /** @var User */ - $user = $this->Users->patchEntity($user, $data); - $errors = $user->getErrors(); - if (empty($errors) && $this->Users->save($user)) { - return true; - } else { - $this->Flash->set( - __('The user could not be saved. Please, try again.'), - ['element' => 'error'] - ); + if (!$failure) { + $result = $this->Users->deleteAllExceptEntries($id); + if (empty($result)) { + $failure = true; + $this->Flash->set(__('user.del.fail.2'), ['element' => 'error']); } } - $this->set('user', $user); - return $user; + if ($failure) { + return $this->redirect( + [ + 'prefix' => false, + 'controller' => 'users', + 'action' => 'view', + $id + ] + ); + } + + $this->Flash->set( + __('user.del.ok.m', $readUser->get('username')), + ['element' => 'success'] + ); + + return $this->redirect('/'); } /** @@ -559,23 +617,26 @@ protected function _edit($userId, array $data = null) public function lock() { $form = new BlockForm(); - if (!$this->modLocking || !$form->validate($this->request->getData())) { + if (!$form->validate($this->request->getData())) { throw new BadRequestException; } $id = (int)$this->request->getData('lockUserId'); - if (!$this->Users->exists($id)) { - throw new NotFoundException('User does not exist.', 1524298280); - } + /** @var User */ $readUser = $this->Users->get($id); - if ($id === $this->CurrentUser->getId()) { + $permission = $this->CurrentUser->permission( + 'saito.core.user.lock.set', + (new ResourceAI())->onRole($readUser->getRole()) + ); + if (!$permission) { + throw new ForbiddenException(null, 1571316877); + } + + if ($this->CurrentUser->isUser($readUser)) { $message = __('You can\'t lock yourself.'); $this->Flash->set($message, ['element' => 'error']); - } elseif ($readUser->getRole() === 'admin') { - $message = __('You can\'t lock administrators.'); - $this->Flash->set($message, ['element' => 'error']); } else { try { $duration = (int)$this->request->getData('lockPeriod'); @@ -591,7 +652,8 @@ public function lock() $this->Flash->set($message, ['element' => 'error']); } } - $this->redirect($this->referer()); + + return $this->redirect($this->referer()); } /** @@ -600,13 +662,26 @@ public function lock() * @param string $id user-ID * @return void */ - public function unlock($id) + public function unlock(string $id) { - $user = $this->Users->UserBlocks->findById($id)->contain(['Users'])->first(); + $id = (int)$id; - if (!$id || !$this->modLocking) { - throw new BadRequestException; + /** @var User */ + $user = $this->Users + ->find() + ->matching('UserBlocks', function ($q) use ($id) { + return $q->where(['UserBlocks.id' => $id]); + }) + ->first(); + + $permission = $this->CurrentUser->permission( + 'saito.core.user.lock.set', + (new ResourceAI())->onRole($user->getRole()) + ); + if (!$permission) { + throw new ForbiddenException(null, 1571316877); } + if (!$this->Users->UserBlocks->unblock($id)) { $this->Flash->set( __('Error while unlocking.'), @@ -614,7 +689,7 @@ public function unlock($id) ); } - $message = __('User {0} is unlocked.', $user->user->get('username')); + $message = __('User {0} is unlocked.', $user->get('username')); $this->Flash->set($message, ['element' => 'success']); $this->redirect($this->referer()); } @@ -629,12 +704,13 @@ public function unlock($id) */ public function changepassword($id = null) { - if (!$id) { + if (empty($id)) { throw new BadRequestException(); } + /** @var User */ $user = $this->Users->get($id); - $allowed = $this->_isEditingAllowed($this->CurrentUser, $id); + $allowed = $this->CurrentUser->isUser($user); if (empty($user) || !$allowed) { throw new SaitoForbiddenException( "Attempt to change password for user $id.", @@ -691,15 +767,16 @@ public function changepassword($id = null) */ public function setpassword($id) { - if (!$this->CurrentUser->permission('saito.core.user.password.set')) { + /** @var User */ + $user = $this->Users->get($id); + + if (!$this->CurrentUser->permission('saito.core.user.password.set', (new ResourceAI())->onRole($user->getRole()))) { throw new SaitoForbiddenException( "Attempt to set password for user $id.", ['CurrentUser' => $this->CurrentUser] ); } - $user = $this->Users->get($id); - if ($this->getRequest()->is('post')) { $this->Users->patchEntity($user, $this->getRequest()->getData(), ['fields' => 'password']); @@ -723,6 +800,52 @@ public function setpassword($id) $this->set(compact('user')); } + /** + * View and set user role + * + * @param string $id User-ID + * @return void|Response + */ + public function role($id) + { + /** @var User */ + $user = $this->Users->get($id); + $identifier = (new ResourceAI())->onRole($user->getRole()); + $unrestricted = $this->CurrentUser->permission('saito.core.user.role.set.unrestricted', $identifier); + $restricted = $this->CurrentUser->permission('saito.core.user.role.set.restricted', $identifier); + if (!$restricted && !$unrestricted) { + throw new ForbiddenException(); + } + + /** @var Permissions */ + $Permissions = Registry::get('Permissions'); + + $roles = $Permissions->getRoles()->get($this->CurrentUser->getRole(), false, $unrestricted); + + if ($this->getRequest()->is('put')) { + $type = $this->getRequest()->getData('user_type'); + if (!in_array($type, $roles)) { + throw new \InvalidArgumentException( + sprintf('User type "%s" is not available.', $type), + 1573376871 + ); + } + $patched = $this->Users->patchEntity($user, ['user_type' => $type]); + + $errors = $patched->getErrors(); + if (empty($errors)) { + $this->Users->save($patched); + + return $this->redirect(['action' => 'edit', $user->get('id')]); + } + + $msg = current(current($errors)); + $this->Flash->set($msg, ['element' => 'error']); + } + + $this->set(compact('roles', 'user')); + } + /** * Set slidetab-order. * @@ -803,9 +926,8 @@ public function beforeFilter(Event $event) $this->Security->setConfig('unlockedActions', $unlocked); $this->Authentication->allowUnauthenticated(['login', 'logout', 'register', 'rs']); - $this->modLocking = $this->CurrentUser - ->permission('saito.core.user.block'); - $this->set('modLocking', $this->modLocking); + $this->AuthUser->authorizeAction('register', 'saito.core.user.register'); + $this->AuthUser->authorizeAction('rs', 'saito.core.user.register'); // Login form times-out and degrades user experience. // See https://github.com/Schlaefer/Saito/issues/339 @@ -817,22 +939,6 @@ public function beforeFilter(Event $event) Stopwatch::stop('Users->beforeFilter()'); } - /** - * Checks if the current user is allowed to edit user $userId - * - * @param CurrentUserInterface $CurrentUser user - * @param int $userId user-ID - * @return bool - */ - protected function _isEditingAllowed(CurrentUserInterface $CurrentUser, $userId) - { - if ($CurrentUser->permission('saito.core.user.edit')) { - return true; - } - - return $CurrentUser->getId() === (int)$userId; - } - /** * Logout user if logged in and create response to revisit logged out * diff --git a/src/Lib/Model/Table/AppTable.php b/src/Lib/Model/Table/AppTable.php index 0e33a9064..044b3d114 100644 --- a/src/Lib/Model/Table/AppTable.php +++ b/src/Lib/Model/Table/AppTable.php @@ -17,7 +17,6 @@ use Cake\Database\Expression\QueryExpression; use Cake\Event\Event; use Cake\Event\EventManager; -use Cake\ORM\Entity; use Cake\ORM\Table; use Saito\Event\SaitoEventManager; @@ -127,7 +126,7 @@ protected function _dispatchEvent($event, $data = []) */ public function dispatchSaitoEvent($event, $data) { - if (!$this->_SEM) { + if (empty($this->_SEM)) { $this->_SEM = SaitoEventManager::getInstance(); } $this->_SEM->dispatch($event, $data + ['Model' => $this]); diff --git a/src/Lib/Saito/App/Registry.php b/src/Lib/Saito/App/Registry.php index 4f7776dae..ac9f3248f 100644 --- a/src/Lib/Saito/App/Registry.php +++ b/src/Lib/Saito/App/Registry.php @@ -13,9 +13,12 @@ namespace Saito\App; use Aura\Di\Container; +use Aura\Di\ContainerBuilder; use Cake\Core\Configure; +use Cake\ORM\TableRegistry; use Cron\Lib\Cron; use Saito\Markup\MarkupSettings; +use Saito\User\Permission\Permissions; /** * Global registry for Saito app. @@ -24,32 +27,35 @@ */ class Registry { - /** * @var Container; */ - protected static $_DIC; + protected static $dic; /** - * Initialize + * Resets and initializes registry. * * @return Container */ public static function initialize() { - $dic = new Container(new \Aura\Di\Factory); + $dic = (new ContainerBuilder())->newInstance(); $dic->set('Cron', new Cron()); - $dic->set('Permission', $dic->lazyNew('Saito\User\Permission')); + + $dic->set('Permissions', $dic->lazyNew(Permissions::class)); + $dic->params[Permissions::class]['roles'] = Configure::read('Saito.Permission.Roles'); + $dic->params[Permissions::class]['resources'] = Configure::read('Saito.Permission.Resources'); + $dic->params[Permissions::class]['categories'] = TableRegistry::getTableLocator()->get('Categories'); + $dic->set('AppStats', $dic->lazyNew('\Saito\App\Stats')); - $dic->params['\Saito\Posting\Posting']['CurrentUser'] = $dic->lazyGet('CU'); $dic->set('MarkupSettings', $dic->lazyNew(MarkupSettings::class)); $markupClass = Configure::read('Saito.Settings.ParserPlugin'); - ; + $dic->set('Markup', $dic->lazyNew($markupClass)); $dic->params[$markupClass]['settings'] = $dic->lazyGet('MarkupSettings'); - self::$_DIC = $dic; + self::$dic = $dic; return $dic; } @@ -61,9 +67,9 @@ public static function initialize() * @param object $object object * @return void */ - public static function set($key, $object) + public static function set(string $key, object $object) { - self::$_DIC->set($key, $object); + self::$dic->set($key, $object); } /** @@ -72,9 +78,9 @@ public static function set($key, $object) * @param string $key key * @return object */ - public static function get($key) + public static function get(string $key): object { - return self::$_DIC->get($key); + return self::$dic->get($key); } /** @@ -85,11 +91,8 @@ public static function get($key) * @param array $setter setter * @return object */ - public static function newInstance( - $key, - array $params = [], - array $setter = [] - ) { - return self::$_DIC->newInstance($key, $params, $setter); + public static function newInstance($key, array $params = [], array $setter = []): object + { + return self::$dic->newInstance($key, $params, $setter); } } diff --git a/src/Lib/Saito/Cache/CacheSupport.php b/src/Lib/Saito/Cache/CacheSupport.php index 6b1c78af0..4dcbe3b4f 100644 --- a/src/Lib/Saito/Cache/CacheSupport.php +++ b/src/Lib/Saito/Cache/CacheSupport.php @@ -249,7 +249,7 @@ public function implementedEvents() public function implementedSaitoEvents() { return [ - 'Model.Saito.Posting.delete' => 'onDelete' + 'Model.Saito.Postings.delete' => 'onDelete' ]; } diff --git a/src/Lib/Saito/Cache/ItemCache.php b/src/Lib/Saito/Cache/ItemCache.php index e69cc683f..313c3c7b3 100644 --- a/src/Lib/Saito/Cache/ItemCache.php +++ b/src/Lib/Saito/Cache/ItemCache.php @@ -159,7 +159,7 @@ public function set($key, $content, $timestamp = null) $this->_read(); } - if (!$timestamp) { + if ($timestamp === null) { $timestamp = $this->_now; } diff --git a/src/Lib/Saito/Exception/Logger/ExceptionLogger.php b/src/Lib/Saito/Exception/Logger/ExceptionLogger.php index 2a852eca5..4c89ecc5d 100644 --- a/src/Lib/Saito/Exception/Logger/ExceptionLogger.php +++ b/src/Lib/Saito/Exception/Logger/ExceptionLogger.php @@ -79,7 +79,7 @@ public function write($message, $data = null) if ($request) { $data = $request->getData(); - if ($request && !empty($data)) { + if (!empty($data)) { $this->_add($this->_filterData($data), 'Data'); } } diff --git a/src/Lib/Saito/JsData/Notifications.php b/src/Lib/Saito/JsData/Notifications.php index 60765caa2..8378672b8 100644 --- a/src/Lib/Saito/JsData/Notifications.php +++ b/src/Lib/Saito/JsData/Notifications.php @@ -31,24 +31,18 @@ public function add(string $message, ?array $options = []): void ]; $options = array_merge($defaults, $options); - if (!is_array($message)) { - $message = [$message]; + $nm = [ + 'message' => $message, + 'type' => $options['type'], + 'channel' => $options['channel'] + ]; + if (isset($options['title'])) { + $nm['title'] = $options['title']; } - - foreach ($message as $m) { - $nm = [ - 'message' => $m, - 'type' => $options['type'], - 'channel' => $options['channel'] - ]; - if (isset($options['title'])) { - $nm['title'] = $options['title']; - } - if (isset($options['element'])) { - $nm['element'] = $options['element']; - } - $this->notifications[] = $nm; + if (isset($options['element'])) { + $nm['element'] = $options['element']; } + $this->notifications[] = $nm; } /** diff --git a/src/Lib/Saito/Posting/Basic/BasicPostingInterface.php b/src/Lib/Saito/Posting/Basic/BasicPostingInterface.php index e352c8fbd..29f23db80 100644 --- a/src/Lib/Saito/Posting/Basic/BasicPostingInterface.php +++ b/src/Lib/Saito/Posting/Basic/BasicPostingInterface.php @@ -20,10 +20,10 @@ interface BasicPostingInterface /** * Get posting property * - * @param string $var kehy + * @param string $var key * @return mixed */ - public function get($var); + public function get(string $var); /** * Check if posting is locked. diff --git a/src/Lib/Saito/Posting/Basic/BasicPostingTrait.php b/src/Lib/Saito/Posting/Basic/BasicPostingTrait.php index c0d9ac37a..3f7024f7f 100644 --- a/src/Lib/Saito/Posting/Basic/BasicPostingTrait.php +++ b/src/Lib/Saito/Posting/Basic/BasicPostingTrait.php @@ -17,6 +17,11 @@ */ trait BasicPostingTrait { + /** + * {@inheritDoc} + */ + abstract public function get(string $var); + /** * {@inheritDoc} */ diff --git a/src/Lib/Saito/Posting/Decorator/AbstractPostingDecorator.php b/src/Lib/Saito/Posting/Decorator/AbstractPostingDecorator.php index 14d0f17c1..dc79f3347 100644 --- a/src/Lib/Saito/Posting/Decorator/AbstractPostingDecorator.php +++ b/src/Lib/Saito/Posting/Decorator/AbstractPostingDecorator.php @@ -149,7 +149,7 @@ public function addDecorator(callable $fct) /** * {@inheritDoc} */ - public function map(callable $callback, bool $mapSelf = true, PostingInterface $node = null): void + public function map(callable $callback, bool $mapSelf = true, $node = null): void { $this->_Posting->map($callback, $mapSelf, $node); } diff --git a/src/Lib/Saito/Posting/Posting.php b/src/Lib/Saito/Posting/Posting.php index 51c47629a..e490cc584 100644 --- a/src/Lib/Saito/Posting/Posting.php +++ b/src/Lib/Saito/Posting/Posting.php @@ -16,7 +16,7 @@ use Saito\Posting\PostingInterface; use Saito\Posting\UserPosting\UserPostingTrait; use Saito\Thread\Thread; -use Saito\User\ForumsUserInterface; +use Saito\User\CurrentUser\CurrentUserInterface; use Saito\User\RemovedSaitoUser; use Saito\User\SaitoUser; @@ -42,13 +42,11 @@ class Posting implements PostingInterface /** * Constructor. * - * @param ForumsUserInterface $CurrentUser current-user * @param array $rawData raw posting data * @param array $options options * @param null|Thread $tree thread */ public function __construct( - ForumsUserInterface $CurrentUser, $rawData, array $options = [], Thread $tree = null @@ -61,8 +59,6 @@ public function __construct( $this->_rawData['user'] = new SaitoUser($this->_rawData['user']); } - $this->setCurrentUser($CurrentUser); - $options += ['level' => 0]; $this->_level = $options['level']; @@ -92,6 +88,19 @@ public function get($var) } } + /** + * {@inheritDoc} + */ + public function withCurrentUser(CurrentUserInterface $CU): self + { + $this->setCurrentUser($CU); + $this->map(function ($node) use ($CU) { + $node->setCurrentUser($CU); + }); + + return $this; + } + /** * {@inheritDoc} */ @@ -151,7 +160,7 @@ public function hasAnswers() /** * {@inheritDoc} */ - public function map(callable $callback, bool $mapSelf = true, PostingInterface $node = null): void + public function map(callable $callback, bool $mapSelf = true, $node = null): void { if ($node === null) { $node = $this; @@ -191,7 +200,6 @@ protected function _attachChildren() if (isset($this->_rawData['_children'])) { foreach ($this->_rawData['_children'] as $child) { $this->_children[] = new Posting( - $this->getCurrentUser(), $child, ['level' => $this->_level + 1], $this->_Thread diff --git a/src/Lib/Saito/Posting/PostingInterface.php b/src/Lib/Saito/Posting/PostingInterface.php index 6e9eb6c14..31a066b14 100644 --- a/src/Lib/Saito/Posting/PostingInterface.php +++ b/src/Lib/Saito/Posting/PostingInterface.php @@ -13,73 +13,17 @@ namespace Saito\Posting; use Saito\Posting\Basic\BasicPostingInterface; +use Saito\Posting\ThreadNode\ThreadNodeInterface; use Saito\Posting\UserPosting\UserPostingInterface; -use Saito\Thread\Thread; +use Saito\User\CurrentUser\CurrentUserInterface; -/** - * Posting properties derived from the other postings building a (sub)thread - */ -interface PostingInterface extends BasicPostingInterface, UserPostingInterface +interface PostingInterface extends BasicPostingInterface, ThreadNodeInterface, UserPostingInterface { /** - * Get sub-postings one level below this posting (direct answers) - * - * @return array of postings - */ - public function getChildren(); - - /** - * Get all sub-postings on all level below this postings - * - * @return array of postings - */ - public function getAllChildren(); - - /** - * Get level of posting in thread - * - * @return int - */ - public function getLevel(): int; - - /** - * Get thread for posting. - * - * @return Thread - */ - public function getThread(); - - /** - * Check if posting has answers. - * - * @return bool - */ - public function hasAnswers(); - - /** - * Map posting and all children to callback - * - * @param callable $callback callback - * @param bool $mapSelf map this posting - * @param PostingInterface $node root posting for callbacks to apply - * @return void - */ - public function map(callable $callback, bool $mapSelf = true, PostingInterface $node = null): void; - - /** - * Get raw posting data - * - * @td @sm @perf Benchmark and remove if O.K. - * - * @return array - */ - public function toArray(); - - /** - * Attach decorators to posting. + * Set current user for UserPostingTrait * - * @param callable $fct callback - * @return PostingInterface new posting + * @param CurrentUserInterface $CU The current user + * @return mixed */ - public function addDecorator(callable $fct); + public function withCurrentUser(CurrentUserInterface $CU); } diff --git a/src/Lib/Saito/Posting/ThreadNode/ThreadNodeInterface.php b/src/Lib/Saito/Posting/ThreadNode/ThreadNodeInterface.php new file mode 100644 index 000000000..817d8cb87 --- /dev/null +++ b/src/Lib/Saito/Posting/ThreadNode/ThreadNodeInterface.php @@ -0,0 +1,83 @@ +_CurrentUser; } /** - * Set current user. - * - * @param CurrentUserInterface $CurrentUser current user - * @return void + * {@inheritDoc} */ - public function setCurrentUser($CurrentUser) + public function setCurrentUser(CurrentUserInterface $CurrentUser): void { $this->_CurrentUser = $CurrentUser; } @@ -109,13 +105,25 @@ protected function _isEditingAllowed(BasicPostingInterface $posting, CurrentUser return true; } + /// Check category + $action = $posting->isRoot() ? 'thread' : 'answer'; + $categoryAllowed = $User->getCategories() + ->permission($action, $posting->get('category_id')); + if (!$categoryAllowed) { + return false; + } + $editPeriod = Configure::read('Saito.Settings.edit_period') * 60; $timeLimit = $editPeriod + ($posting->get('time')->format('U')); $isOverTime = time() > $timeLimit; - $isOwn = $User->getId() === $posting->get('user_id'); + $isOwn = $User->permission( + 'saito.core.posting.edit', + (new ResourceAI())->onOwner($posting->get('user_id')) + ); if (!$isOverTime && $isOwn && !$this->isLocked()) { + // Normal posting without special conditions. return true; } @@ -141,14 +149,14 @@ public function isIgnored(): bool */ public function isUnread(): bool { - if (!isset($this->_cache['isUnread'])) { + if (!isset($this->_userPostingTraitUnreadCache['isUnread'])) { $id = $this->get('id'); $time = $this->get('time'); - $this->_cache['isUnread'] = !$this->getCurrentUser() + $this->_userPostingTraitUnreadCache['isUnread'] = !$this->getCurrentUser() ->getReadPostings()->isRead($id, $time); } - return $this->_cache['isUnread']; + return $this->_userPostingTraitUnreadCache['isUnread']; } /** diff --git a/src/Lib/Saito/Test/AssertTrait.php b/src/Lib/Saito/Test/AssertTrait.php index 0a0158697..0a2e0908c 100644 --- a/src/Lib/Saito/Test/AssertTrait.php +++ b/src/Lib/Saito/Test/AssertTrait.php @@ -12,6 +12,7 @@ namespace Saito\Test; +use AssertionError; use Symfony\Component\DomCrawler\Crawler; /** @@ -95,4 +96,35 @@ protected function _getDOMXPath($html) return $xpath; } + + /** + * Assert Flash message was set + * + * @param string $message message + * @param string $element element + * @param bool $debug debugging + * @return void + */ + protected function assertFlash(string $message, string $element = null, $debug = false): void + { + if ($debug) { + debug($_SESSION['Flash']['flash']); + } + if (!empty($_SESSION['Flash']['flash'])) { + foreach ($_SESSION['Flash']['flash'] as $flash) { + if ($flash['message'] !== $message) { + continue; + } + if ($element !== null && $flash['element'] !== 'Flash/' . $element) { + continue; + } + + return; + } + } + + throw new AssertionError( + sprintf('Flash message "%s" was not set.', $message) + ); + } } diff --git a/src/Lib/Saito/Test/IntegrationTestCase.php b/src/Lib/Saito/Test/IntegrationTestCase.php index 13808a1fd..4ea2b25d4 100644 --- a/src/Lib/Saito/Test/IntegrationTestCase.php +++ b/src/Lib/Saito/Test/IntegrationTestCase.php @@ -48,11 +48,10 @@ abstract class IntegrationTestCase extends TestCase */ public function setUp() { - parent::setUp(); $this->disableErrorHandlerMiddleware(); $this->setUpSaito(); - $this->_clearCaches(); $this->markUpdated(); + parent::setUp(); } /** @@ -60,11 +59,13 @@ public function setUp() */ public function tearDown() { + parent::tearDown(); + // This will restore the Config. Leave it after parent::tearDown() or + // Cake's Configure restore will overwrite it and mess things up. $this->tearDownSaito(); $this->_unsetAjax(); $this->_unsetJson(); $this->_unsetUserAgent(); - parent::tearDown(); $this->_clearCaches(); } @@ -325,6 +326,8 @@ public function assertRedirectLogin($redirectUrl = null, string $msg = '') ], true); $redirectHeader = $response->getHeader('Location')[0]; $this->assertEquals($expected, $redirectHeader, $msg); + $this->assertResponseEmpty(); + $this->assertResponseCode(302); } /** diff --git a/src/Lib/Saito/Test/TestCaseTrait.php b/src/Lib/Saito/Test/TestCaseTrait.php index c4f78238c..648ff6ac3 100644 --- a/src/Lib/Saito/Test/TestCaseTrait.php +++ b/src/Lib/Saito/Test/TestCaseTrait.php @@ -15,23 +15,17 @@ use Cake\Core\Configure; use Cake\Event\EventManager; use Cake\Filesystem\File; +use Cake\I18n\I18n; use Cake\Mailer\TransportFactory; use Cake\Utility\Inflector; -use Cron\Lib\Cron; use Saito\App\Registry; use Saito\Cache\CacheSupport; -use Saito\User\ForumsUserInterface; -use Saito\User\SaitoUser; trait TestCaseTrait { + private $saitoSettings; - /** - * @var \Aura\Di\Container - */ - protected $dic; - - protected $saitoSettings; + protected $saitoPermissions; /** * set-up saito @@ -40,7 +34,8 @@ trait TestCaseTrait */ protected function setUpSaito() { - $this->initDic(); + Registry::initialize(); + $this->_storeSettings(); $this->mockMailTransporter(); $this->_clearCaches(); @@ -70,23 +65,6 @@ protected function _clearCaches() unset($CacheSupport); } - /** - * Setup for dependency injection container - * - * @param ForumsUserInterface $User user - * @return void - */ - public function initDic(ForumsUserInterface $User = null) - { - $this->dic = Registry::initialize(); - if ($User === null) { - $User = new SaitoUser(); - } - $this->dic->set('CU', $User); - - $this->dic->set('Cron', new Cron()); - } - /** * store global settings * @@ -95,7 +73,8 @@ public function initDic(ForumsUserInterface $User = null) protected function _storeSettings() { $this->saitoSettings = Configure::read('Saito.Settings'); - Configure::write('Saito.language', 'en'); + $this->saitoPermissions = clone(Configure::read('Saito.Permission.Resources')); + $this->setI18n('en'); Configure::write('Saito.Settings.ParserPlugin', \Plugin\BbcodeParser\src\Lib\Markup::class); Configure::write('Saito.Settings.uploader', clone($this->saitoSettings['uploader'])); } @@ -107,9 +86,20 @@ protected function _storeSettings() */ protected function _restoreSettings() { - if ($this->saitoSettings !== null) { - Configure::write('Saito.Settings', $this->saitoSettings); - } + Configure::write('Saito.Settings', $this->saitoSettings); + Configure::write('Saito.Permission.Resources', $this->saitoPermissions); + } + + /** + * Set the current translation language + * + * @param string $lang language code + * @return void + */ + public function setI18n(string $lang): void + { + Configure::write('Saito.language', $lang); + I18n::setLocale($lang); } /** diff --git a/src/Lib/Saito/User/CurrentUser/CurrentUserFactory.php b/src/Lib/Saito/User/CurrentUser/CurrentUserFactory.php index f78c5d171..9292a6f31 100644 --- a/src/Lib/Saito/User/CurrentUser/CurrentUserFactory.php +++ b/src/Lib/Saito/User/CurrentUser/CurrentUserFactory.php @@ -87,7 +87,7 @@ public static function createVisitor(Controller $controller, ?array $config = [] */ public static function createDummy(?array $config = []): CurrentUserInterface { - $config['user_type'] = 'anon'; + $config['user_type'] = $config['user_type'] ?? 'anon'; $CurrentUser = new CurrentUser($config); $CurrentUser->setCategories(new Categories($CurrentUser)); diff --git a/src/Lib/Saito/User/CurrentUser/CurrentUserInterface.php b/src/Lib/Saito/User/CurrentUser/CurrentUserInterface.php index d27803dad..6feafc3e2 100644 --- a/src/Lib/Saito/User/CurrentUser/CurrentUserInterface.php +++ b/src/Lib/Saito/User/CurrentUser/CurrentUserInterface.php @@ -86,4 +86,11 @@ public function ignores($userId); * @return bool */ public function isLoggedIn(): bool; + + /** + * Get all settings + * + * @return array + */ + public function getSettings(): array; } diff --git a/src/Lib/Saito/User/ForumsUserInterface.php b/src/Lib/Saito/User/ForumsUserInterface.php index 25cc243ac..e7dad1350 100644 --- a/src/Lib/Saito/User/ForumsUserInterface.php +++ b/src/Lib/Saito/User/ForumsUserInterface.php @@ -12,7 +12,7 @@ namespace Saito\User; -use App\Model\Entity\User; +use Saito\User\Permission\ResourceAI; interface ForumsUserInterface { @@ -33,21 +33,6 @@ public function get($setting); */ public function set(string $setting, $value); - /** - * Sets all (and replaces existing) settings for a user - * - * @param array $settings Settings - * @return void - */ - public function setSettings(array $settings): void; - - /** - * Get all settings - * - * @return array - */ - public function getSettings(): array; - /** * Get user's id. * @@ -84,11 +69,19 @@ public function isActivated(): bool; */ public function isUser(ForumsUserInterface $user): bool; + /** + * Get number of postings + * + * @return int + */ + public function numberOfPostings(): int; + /** * Check if user has permission to access a resource. * * @param string $resource resource + * @param ResourceAI $identity Identity * @return bool */ - public function permission(string $resource): bool; + public function permission(string $resource, ResourceAI $identity = null): bool; } diff --git a/src/Lib/Saito/User/ForumsUserTrait.php b/src/Lib/Saito/User/ForumsUserTrait.php new file mode 100644 index 000000000..729e67aad --- /dev/null +++ b/src/Lib/Saito/User/ForumsUserTrait.php @@ -0,0 +1,91 @@ +get('id'); + } + + /** + * {@inheritDoc} + */ + public function isUser(ForumsUserInterface $user): bool + { + return $user->getId() === $this->getId(); + } + + /** + * Checks if user is forbidden. + * + * @return bool + */ + public function isLocked(): bool + { + return (bool)$this->get('user_lock'); + } + + /** + * Checks if user is forbidden. + * + * @return bool + */ + public function isActivated(): bool + { + return !$this->get('activate_code'); + } + + /** + * Get role. + * + * @return string + */ + public function getRole(): string + { + return $this->get('user_type'); + } + + /** + * {@inheritDoc} + */ + public function numberOfPostings(): int + { + return $this->get('entry_count'); + } + + /** + * {@inheritDoc} + */ + public function permission(string $resource, ResourceAI $identity = null): bool + { + if ($identity === null) { + $identity = new ResourceAI(); + } + + $permissions = Registry::get('Permissions'); + + return $permissions->check($resource, $identity->asUser($this)); + } +} diff --git a/src/Lib/Saito/User/Permission.php b/src/Lib/Saito/User/Permission.php deleted file mode 100644 index 8ffe48950..000000000 --- a/src/Lib/Saito/User/Permission.php +++ /dev/null @@ -1,195 +0,0 @@ - true, - 'user' => ['anon'], - 'mod' => ['user'], - 'admin' => ['mod'] - ]; - - protected $resources = [ - 'saito.core.admin.backend' => ['admin' => true], - 'saito.core.posting.edit.restricted' => ['mod' => true], - 'saito.core.posting.edit.unrestricted' => ['admin' => true], - 'saito.core.user.activate' => ['admin' => true], - 'saito.core.user.block' => ['admin' => true], - 'saito.core.user.password.set' => ['admin' => true], - 'saito.core.user.delete' => ['admin' => true], - 'saito.core.user.edit' => ['admin' => true], - 'saito.core.user.contact' => ['admin' => true], - 'saito.core.view.ip' => ['mod' => true], - // = controller actions = - // @td @sm make action specific instead of generic group names - 'anon' => ['anon' => true], - 'user' => ['user' => true], - 'mod' => ['mod' => true], - 'admin' => ['admin' => true], - ]; - - /** - * constructor - */ - public function __construct() - { - $this->_bootstrap(); - } - - /** - * allow resource - * - * @param string $role role - * @param string $resource resource - * @return void - */ - public function allow($role, $resource) - { - $this->resources[$resource][$role] = true; - } - - /** - * disallow resource - * - * @param string $role role - * @param string $resource resource - * @return void - */ - public function disallow($role, $resource) - { - unset($this->resources[$resource][$role]); - } - - /** - * Check if access to resource is allowed. - * - * @param string $role role - * @param string $resource resource - * @return bool - */ - public function check($role, $resource) - { - $roles = $this->_getRoles($role); - if (!empty($roles)) { - foreach ($roles as $role) { - if (isset($this->resources[$resource][$role])) { - return true; - } - } - } - - return false; - } - - /** - * resolves role and add groups - * - * @param string $role role - * @return mixed - */ - protected function _getRoles($role) - { - $key = 'saito.core.permission.' . $role; - - return $this->rememberStatic( - $key, - function () use ($role) { - if (!isset($this->groups[$role])) { - return false; - } - if ($this->groups[$role] === true) { - return [$role]; - } elseif (is_array($this->groups[$role])) { - $roles = [$role]; - foreach ($this->groups[$role] as $role) { - $roles = array_merge($roles, $this->_getRoles($role)); - } - - return $roles; - } - - return false; - } - ); - } - - /** - * bootstrap resources - * - * @return void - */ - protected function _bootstrap() - { - Stopwatch::start('Permission::__construct()'); - $this->resources = Cache::remember( - 'saito.core.permission.resources', - function () { - $this->_bootstrapCategories(); - - return $this->resources; - } - ); - Stopwatch::stop('Permission::__construct()'); - } - - /** - * convert category-accessions and insert them as resources - * - * `saito.core.category..` - * - * @return void - */ - protected function _bootstrapCategories() - { - if (Configure::read('Saito.Settings.block_user_ui')) { - $this->allow('mod', 'saito.core.user.block'); - } - - /** @var CategoriesTable */ - $Categories = TableRegistry::get('Categories'); - $categories = $Categories->getAllCategories(); - $accessions = [0 => 'anon', 1 => 'user', 2 => 'mod', 3 => 'admin']; - $actions = [ - 'read' => 'accession', - 'thread' => 'accession_new_thread', - 'answer' => 'accession_new_posting' - ]; - foreach ($categories as $category) { - foreach ($actions as $action => $field) { - $role = $accessions[$category->get($field)]; - $categoryId = $category->get('id'); - $resource = "saito.core.category.{$categoryId}.{$action}"; - $this->allow($role, $resource); - } - } - } -} diff --git a/src/Lib/Saito/User/Permission/Permissions.php b/src/Lib/Saito/User/Permission/Permissions.php new file mode 100644 index 000000000..8ef5be3e9 --- /dev/null +++ b/src/Lib/Saito/User/Permission/Permissions.php @@ -0,0 +1,131 @@ +roles = $roles; + $this->resources = $resources; + $this->categories = $categories; + + $categories = Cache::remember( + 'saito.core.permission.categories', + function () { + return $this->bootstrapCategories(); + } + ); + foreach ($categories as $category) { + $this->resources->add( + (new Resource($category['resource'])) + ->allow((new ResourceAC()) + ->asRole($category['role'])) + ); + } + + Stopwatch::stop('Permission::__construct()'); + } + + /** + * Check if access to resource is allowed. + * + * @param string $resource Resource to check + * @param ResourceAI $identifier Identifier to provide + * @return bool + */ + public function check(string $resource, ResourceAI $identifier): bool + { + $resource = $this->resources->get($resource); + + return $resource === null ? false : $resource->check($identifier); + } + + /** + * Gets the roles object + * + * @return Roles + */ + public function getRoles(): Roles + { + return $this->roles; + } + + /** + * convert category-accessions and insert them as resources + * + * Resource: `saito.core.category..` + * + * @return array [['resource' => , 'role' => ]] + */ + protected function bootstrapCategories(): array + { + $resources = []; + $categories = $this->categories->getAllCategories(); + $roles = $this->roles->getAvailable(true); + $accessions = array_combine(array_column($roles, 'id'), array_column($roles, 'type')); + $actions = [ + 'read' => 'accession', + 'thread' => 'accession_new_thread', + 'answer' => 'accession_new_posting' + ]; + foreach ($categories as $category) { + foreach ($actions as $action => $field) { + if (empty($accessions[$category->get($field)])) { + continue; + } + $role = $accessions[$category->get($field)]; + $categoryId = $category->get('id'); + $resource = "saito.core.category.{$categoryId}.{$action}"; + $resources[] = ['resource' => $resource, 'role' => $role]; + } + } + + return $resources; + } +} diff --git a/src/Lib/Saito/User/Permission/Resource.php b/src/Lib/Saito/User/Permission/Resource.php new file mode 100644 index 000000000..64ced1e6b --- /dev/null +++ b/src/Lib/Saito/User/Permission/Resource.php @@ -0,0 +1,96 @@ +name = $name; + } + + /** + * Get resource name + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Allow the resource on a permission + * + * @param ResourceAC $permission permission + * @return self + */ + public function allow(ResourceAC $permission): self + { + $permission->lock(); + $this->allowed[] = $permission; + + return $this; + } + + /** + * Disallow the resource on a permission + * + * @param ResourceAC $permission permission + * @return self + */ + public function disallow(ResourceAC $permission): self + { + $permission->lock(); + $this->disallowed[] = $permission; + + return $this; + } + + /** + * Check resource against identity + * + * @param ResourceAI $identity Identity + * @return bool + */ + public function check(ResourceAI $identity): bool + { + foreach ($this->disallowed as $permission) { + if ($permission->check($identity)) { + return false; + } + } + + foreach ($this->allowed as $permission) { + if ($permission->check($identity)) { + return true; + } + } + + return false; + } +} diff --git a/src/Lib/Saito/User/Permission/ResourceAC.php b/src/Lib/Saito/User/Permission/ResourceAC.php new file mode 100644 index 000000000..87adaa560 --- /dev/null +++ b/src/Lib/Saito/User/Permission/ResourceAC.php @@ -0,0 +1,177 @@ + => true] */ + protected $asRole = []; + + /** @var array Roles as array_keys [ => true] */ + protected $onRole = []; + + protected $onOwn = false; + + protected $everybody = false; + + protected $locked = false; + + /** + * Lock the permision and disallow further changes + * + * @return self + */ + public function lock(): self + { + $this->locked = true; + + return $this; + } + + /** + * Permission granted as role + * + * @param string $role role + * @return self + */ + public function asRole(string $role): self + { + if ($this->locked) { + $this->handleLocked(); + } + $this->asRole[$role] = true; + + return $this; + } + + /** + * Permission granted on role + * + * @param string $role role + * @return self + */ + public function onRole(string $role): self + { + if ($this->locked) { + $this->handleLocked(); + } + $this->onRole[$role] = true; + + return $this; + } + + /** + * Permissions granted on roles + * + * @param string ...$roles Roles + * @return self + */ + public function onRoles(...$roles): self + { + foreach ($roles as $role) { + $this->onRole($role); + } + + return $this; + } + + /** + * Permission granted on owner + * + * @return self + */ + public function onOwn(): self + { + if ($this->locked) { + $this->handleLocked(); + } + $this->onOwn = true; + + return $this; + } + + /** + * Permission granted for everybody + * + * @return self + */ + public function asEverybody(): self + { + if ($this->locked) { + $this->handleLocked(); + } + $this->everybody = true; + + return $this; + } + + /** + * Check permission against identity-provider + * + * @param ResourceAI $identity identity + * @return bool + */ + public function check(ResourceAI $identity): bool + { + if (!empty($this->onRole)) { + $role = $identity->getRole(); + if ($role === null || !isset($this->onRole[$role])) { + return false; + } + } + + if ($this->everybody === true) { + return true; + } + + if ($this->onOwn === true) { + $CU = $identity->getUser(); + $owner = $identity->getOwner(); + if ($CU !== null && $owner !== null && $CU->getId() === $owner) { + return true; + } + } + + if (!empty($this->asRole)) { + $CU = $identity->getUser(); + if ($CU !== null) { + // @td Attach to CU + $roles = Registry::get('Permissions')->getRoles(); + $allRoles = $roles->get($CU->getRole()); + foreach ($allRoles as $role) { + if (isset($this->asRole[$role])) { + return true; + } + } + } + } + + return false; + } + + /** + * Handle access to locked permission config + * + * @return void + * @throws \RuntimeException + */ + protected function handleLocked(): void + { + throw new \RuntimeException('PermissionProvider is locked.', 1573820147); + } +} diff --git a/src/Lib/Saito/User/Permission/ResourceAI.php b/src/Lib/Saito/User/Permission/ResourceAI.php new file mode 100644 index 000000000..ab6aa882a --- /dev/null +++ b/src/Lib/Saito/User/Permission/ResourceAI.php @@ -0,0 +1,99 @@ +user; + } + + /** + * Get owner-ID of the resource + * + * @return int|null + */ + public function getOwner(): ?int + { + return $this->userId; + } + + /** + * Get owner of the resource + * + * @return string|null + */ + public function getRole(): ?string + { + return $this->role; + } + + /** + * Set a user which requests the permission + * + * @param ForumsUserInterface $user The user. + * @return self + */ + public function asUser(ForumsUserInterface $user): self + { + $this->user = $user; + + return $this; + } + + /** + * Set owner role + * + * @param string $role Owner's role + * @return self + */ + public function onRole(string $role): self + { + $this->role = $role; + + return $this; + } + + /** + * Set owner identity + * + * @param int $userId Owner's user-ID + * @return self + */ + public function onOwner(int $userId): self + { + $this->userId = $userId; + + return $this; + } +} diff --git a/src/Lib/Saito/User/Permission/Resources.php b/src/Lib/Saito/User/Permission/Resources.php new file mode 100644 index 000000000..fcb16613c --- /dev/null +++ b/src/Lib/Saito/User/Permission/Resources.php @@ -0,0 +1,55 @@ +resources[$resource->getName()] = $resource; + + return $this; + } + + /** + * Get resource + * + * @param string $resouce Name of resource to get + * @return \Saito\User\Permission\Resource|null Resource or null of resource not found + */ + public function get(string $resouce): ?Resource + { + return $this->resources[$resouce] ?? null; + } + + /** + * {@inheritDoc} + */ + public function __clone() + { + foreach ($this->resources as $key => $resource) { + $this->resources[$key] = clone $resource; + } + } +} diff --git a/src/Lib/Saito/User/Permission/Roles.php b/src/Lib/Saito/User/Permission/Roles.php new file mode 100644 index 000000000..7b1b3c289 --- /dev/null +++ b/src/Lib/Saito/User/Permission/Roles.php @@ -0,0 +1,110 @@ +roles for easy access with integer ID + * + * @var array + */ + private $rolesAsInt = []; + + /** + * Adds a new role + * + * @param string $role Short title like 'user', 'mod', or 'admin' + * @param int $id A unique id for this role + * @param array $subroles Other roles this role represents + * @return self + */ + public function add(string $role, int $id, array $subroles = []): self + { + $this->roles[$role] = ['id' => $id, 'type' => $role]; + $this->roles[$role]['subroles'] = $subroles; + $this->rolesAsInt[$id] = $this->roles[$role]; + + return $this; + } + + /** + * Get all roles for role + * + * @param string $role Role + * @param bool $includeAnon Include 'anon' user in roles-list + * @param bool $includeOwn If false a list all other roles a user has + * @return array All roles a role has + */ + public function get(string $role, bool $includeAnon = true, bool $includeOwn = true): array + { + if (!isset($this->roles[$role])) { + return []; + } + + $roles = []; + + if ($includeOwn) { + $roles[] = $role; + } + + foreach ($this->roles[$role]['subroles'] as $role) { + if ($role === 'anon' && !$includeAnon) { + continue; + } + $roles[] = $role; + } + + return $roles; + } + + /** + * Get all configured roles + * + * @param bool $includeAnon Include anon user + * @return array + */ + public function getAvailable(bool $includeAnon = false): array + { + $roles = $this->rolesAsInt; + if (!$includeAnon) { + unset($roles[0]); + } + + return $roles; + } + + /** + * Get role id for type + * + * @param string $type Type + * @return int + */ + public function typeToId(string $type): int + { + if (isset($this->roles[$type])) { + return $this->roles[$type]['id']; + } + + throw new RuntimeException(sprintf('Role "%s" not found.', $type)); + } +} diff --git a/src/Lib/Saito/User/SaitoUser.php b/src/Lib/Saito/User/SaitoUser.php index 49bdfe78f..14d7ce3d7 100755 --- a/src/Lib/Saito/User/SaitoUser.php +++ b/src/Lib/Saito/User/SaitoUser.php @@ -13,19 +13,13 @@ namespace Saito\User; use Cake\Utility\Hash; -use Saito\App\Registry; /** * Represents a registered user with all knowledge stored offline */ class SaitoUser implements ForumsUserInterface { - /** - * User ID - * - * @var int - */ - protected $_id = null; + use ForumsUserTrait; /** * User settings @@ -47,14 +41,13 @@ public function __construct(?array $settings = null) } /** - * {@inheritDoc} + * Sets all (and replaces existing) settings for a user + * + * @param array $settings Settings + * @return void */ public function setSettings(array $settings): void { - if (!empty($settings['id'])) { - $this->_id = (int)$settings['id']; - } - $this->_settings = $settings; /// performance cheat @@ -101,69 +94,12 @@ public function set(string $setting, $value) } /** - * {@inheritDoc} + * Get all settings + * + * @return array */ public function getSettings(): array { return $this->_settings; } - - /** - * {@inheritDoc} - */ - public function getId(): int - { - return $this->_id; - } - - /** - * {@inheritDoc} - */ - public function isUser(ForumsUserInterface $user): bool - { - return $user->getId() === $this->getId(); - } - - /** - * Checks if user is forbidden. - * - * @return bool - */ - public function isLocked(): bool - { - return (bool)$this->get('user_lock'); - } - - /** - * Checks if user is forbidden. - * - * @return bool - */ - public function isActivated(): bool - { - return !$this->get('activate_code'); - } - - /** - * Get role. - * - * @return string - */ - public function getRole(): string - { - return $this->get('user_type'); - } - - /** - * Check if user has permission to access a resource. - * - * @param string $resource resource - * @return bool - */ - public function permission(string $resource): bool - { - $permission = Registry::get('Permission'); - - return $permission->check($this->getRole(), $resource); - } } diff --git a/src/Lib/Saito/View/Cell/SlidetabCell.php b/src/Lib/Saito/View/Cell/SlidetabCell.php index 561487db9..87c16dc77 100644 --- a/src/Lib/Saito/View/Cell/SlidetabCell.php +++ b/src/Lib/Saito/View/Cell/SlidetabCell.php @@ -13,7 +13,7 @@ namespace Saito\View\Cell; use Cake\View\Cell; -use Saito\App\Registry; +use Saito\User\CurrentUser\CurrentUserInterface; abstract class SlidetabCell extends Cell { @@ -31,7 +31,7 @@ public function __toString() /** * {@inheritDoc} */ - abstract public function display(); + abstract public function display(CurrentUserInterface $CurrentUser); /** * {@inheritDoc} @@ -45,8 +45,7 @@ abstract protected function _getSlidetabId(); */ protected function _prepareRendering() { - $CurrentUser = Registry::get('CU'); $slidetabId = $this->_getSlidetabId(); - $this->set(compact('CurrentUser', 'slidetabId')); + $this->set(compact('slidetabId')); } } diff --git a/src/Lib/version.php b/src/Lib/version.php index 0d1f59999..05a45a17b 100644 --- a/src/Lib/version.php +++ b/src/Lib/version.php @@ -13,7 +13,7 @@ $config = [ 'Saito' => [ - 'v' => '5.4.1', + 'v' => '5.5.0', 'saitoHomepage' => 'https://saito.siezi.com/' ] ]; diff --git a/src/Locale/de/default.po b/src/Locale/de/default.po index d06c1017d..a209f5b5e 100644 --- a/src/Locale/de/default.po +++ b/src/Locale/de/default.po @@ -145,40 +145,6 @@ msgstr "Keine Log-Datei vorhanden." msgid "Details" msgstr "" -#: plugins/Admin/src/View/Helper/AdminHelper.php:218 -msgid "user.type.anon" -msgstr "Anonym" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:220 -#: src/View/Helper/UserHelper.php:95 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:10 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:18 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:26 -#: src/Template/Cell/SlidetabUserlist/display.ctp:44 -#: src/Template/Users/edit.ctp:36 -msgid "user.type.user" -msgstr "Benutzer" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:222 -#: src/View/Helper/UserHelper.php:97 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:11 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:19 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:27 -#: src/Template/Cell/SlidetabUserlist/display.ctp:41 -#: src/Template/Users/edit.ctp:37 -msgid "user.type.mod" -msgstr "Moderator" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:224 -#: src/View/Helper/UserHelper.php:99 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:12 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:20 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:28 -#: src/Template/Cell/SlidetabUserlist/display.ctp:38 -#: src/Template/Users/edit.ctp:38 -msgid "user.type.admin" -msgstr "Administrator" - #: plugins/Admin/src/View/Helper/SettingHelper.php:121 msgid "edit" msgstr "Bearbeiten" @@ -542,6 +508,9 @@ msgstr "Der Name enthält unerlaubte Sonderzeichen." msgid "vld.users.username.maxlength" msgstr "Name ist zu lang (maximal {0} Zeichen)." +msgid "vld.user.user_type.allowedType" +msgstr "Rolle \"{0}\" ist nicht erlaubt." + #: src/Model/Table/UsersTable.php:275 msgid "error_email_reserved" msgstr "" @@ -1238,7 +1207,7 @@ msgstr "Realer Name" #: src/Template/Users/edit.ctp:93 msgid "user_real_name_exp" -msgstr "(optional)" +msgstr "Realer Name" #: src/Template/Users/edit.ctp:97 src/Template/Users/view.ctp:69 msgid "user_hp" @@ -1246,11 +1215,11 @@ msgstr "Homepage" #: src/Template/Users/edit.ctp:99 msgid "user_hp_exp" -msgstr "(optional)" +msgstr "Homepage" #: src/Template/Users/edit.ctp:107 msgid "user_place_exp" -msgstr "(optional)" +msgstr "Aufenthaltsort" #: src/Template/Users/edit.ctp:109 src/Template/Users/view.ctp:76 msgid "user_place" @@ -1398,10 +1367,8 @@ msgid "login_linkname" msgstr "log in" #: src/Template/Users/setpassword.ctp:12 -#, fuzzy -#| msgid "user.edit.t" msgid "user.pw.set.t" -msgstr "Bearbeite Profil {0}" +msgstr "Setze Passwort für {0}" #: src/Template/Users/view.ctp:34 msgid "user.actv.t" @@ -1455,6 +1422,46 @@ msgstr "Noch keine Beiträge verfasst." msgid "Show all" msgstr "Alle anzeigen" +msgid "user.role.set.btn" +msgstr "Setze Nutzerrolle" + +msgid "user.role.set.t" +msgstr "Setze Nutzerrolle für {0}" + +msgid "user.baseid.set.btn" +msgstr "Setze Basis-ID" + +msgid "user.del.fail.1" +msgstr "Man kann sich nicht selbst löschen." + +msgid "user.del.fail.2" +msgstr "Löschen des Nutzers ist fehlgeschlagen." + +msgid "user.del.fail.2" +msgstr "Please confirm that you want to delete the user." + +msgid "user.del.exp.1" +msgstr "Lösche Nutzer \"{0}\"" + +msgid "user.del.exp.2" +msgstr "Die Profildaten werden gelöscht." + +msgid "user.del.exp.3" +msgstr "Alle Uploads werden gelöscht." + +msgid "user.del.exp.4" +msgstr "Beiträge bleiben erhalten und werden mit unbekannten Verfasser angezeigt." + +msgid "user.del.confirm" +msgstr "Nutzer löschen. Dies kann nicht rückgängig gemacht werden." + +msgid "user.del.btn.t" +msgstr "Nutzer löschen" + +msgid "user.del.ok.m" +msgstr "Nutzer \"{0}\" wurde gelöscht." + + #~ msgid "Ressources" #~ msgstr "Ressourcen" @@ -1662,8 +1669,8 @@ msgstr "Alle anzeigen" #~ msgid "Save" #~ msgstr "Speichern" -#~ msgid "Cancel" -#~ msgstr "Abbrechen" +msgid "Cancel" +msgstr "Abbrechen" #~ msgid "Click here to build your database" #~ msgstr "Datenbank anlegen" @@ -2014,14 +2021,6 @@ msgstr "Alle anzeigen" #~ "werden. Siehe auch." -# -#~ msgid "block_user_ui" -#~ msgstr "Nutzer sperren" - -# -#~ msgid "block_user_ui_exp" -#~ msgstr "Moderatoren dürfen Benutzer sperren." - #~ msgid "store_ip" #~ msgstr "IP speichern" diff --git a/src/Locale/de/nondynamic.po b/src/Locale/de/nondynamic.po index efcd70606..7a1084485 100644 --- a/src/Locale/de/nondynamic.po +++ b/src/Locale/de/nondynamic.po @@ -156,14 +156,6 @@ msgstr "Systemadresse" msgid "email_system_exp" msgstr "Adresse für vom System erzeugten Emails (bspw. Thread-Abonnements). (optional, falls leer wird Hauptadresse verwendet)" -# -msgid "block_user_ui" -msgstr "Nutzer sperren" - -# -msgid "block_user_ui_exp" -msgstr "Moderatoren dürfen Benutzer sperren." - # msgid "store_ip" msgstr "IP speichern" @@ -414,3 +406,22 @@ msgstr "Traurig" msgid "smilies.t.wink" msgstr "Zwinker" + +#******************************************************************************* +#* Permission roles +#******************************************************************************/ + +msgid "permission.role.0" +msgstr "Anonym" + +msgid "permission.role.1" +msgstr "Benutzer" + +msgid "permission.role.2" +msgstr "Moderator" + +msgid "permission.role.3" +msgstr "Administrator" + +msgid "permission.role.4" +msgstr "Eigentümer" diff --git a/src/Locale/en/default.po b/src/Locale/en/default.po index e14bae7f9..f16bccc07 100644 --- a/src/Locale/en/default.po +++ b/src/Locale/en/default.po @@ -140,40 +140,6 @@ msgstr "" msgid "Details" msgstr "" -#: plugins/Admin/src/View/Helper/AdminHelper.php:218 -msgid "user.type.anon" -msgstr "Anonymous" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:220 -#: src/View/Helper/UserHelper.php:95 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:10 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:18 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:26 -#: src/Template/Cell/SlidetabUserlist/display.ctp:44 -#: src/Template/Users/edit.ctp:36 -msgid "user.type.user" -msgstr "User" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:222 -#: src/View/Helper/UserHelper.php:97 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:11 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:19 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:27 -#: src/Template/Cell/SlidetabUserlist/display.ctp:41 -#: src/Template/Users/edit.ctp:37 -msgid "user.type.mod" -msgstr "Moderator" - -#: plugins/Admin/src/View/Helper/AdminHelper.php:224 -#: src/View/Helper/UserHelper.php:99 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:12 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:20 -#: plugins/Admin/src/Template/Element/Categories/edit.ctp:28 -#: src/Template/Cell/SlidetabUserlist/display.ctp:38 -#: src/Template/Users/edit.ctp:38 -msgid "user.type.admin" -msgstr "Administrator" - #: plugins/Admin/src/View/Helper/SettingHelper.php:121 msgid "edit" msgstr "" @@ -534,6 +500,9 @@ msgstr "The name contains forbidden characters." msgid "vld.users.username.maxlength" msgstr "Name is to long ({0} chars max)." +msgid "vld.user.user_type.allowedType" +msgstr "Role \"{0}\" is not allowed." + #: src/Model/Table/UsersTable.php:275 msgid "error_email_reserved" msgstr "Email address is already used." @@ -1216,7 +1185,7 @@ msgstr "Real Name" #: src/Template/Users/edit.ctp:93 msgid "user_real_name_exp" -msgstr "(optional)" +msgstr "Real name" #: src/Template/Users/edit.ctp:97 src/Template/Users/view.ctp:69 msgid "user_hp" @@ -1224,11 +1193,11 @@ msgstr "Homepage" #: src/Template/Users/edit.ctp:99 msgid "user_hp_exp" -msgstr "URL to homepage (optional)" +msgstr "URL to homepage" #: src/Template/Users/edit.ctp:107 msgid "user_place_exp" -msgstr "(optional)" +msgstr "Residence" #: src/Template/Users/edit.ctp:109 src/Template/Users/view.ctp:76 msgid "user_place" @@ -1372,10 +1341,8 @@ msgid "login_linkname" msgstr "Login" #: src/Template/Users/setpassword.ctp:12 -#, fuzzy -#| msgid "user.edit.t" msgid "user.pw.set.t" -msgstr "Edit User {0}" +msgstr "Set Password for {0}" #: src/Template/Users/view.ctp:34 msgid "user.actv.t" @@ -1429,6 +1396,45 @@ msgstr "" msgid "Show all" msgstr "" +msgid "user.role.set.btn" +msgstr "Set User Role" + +msgid "user.role.set.t" +msgstr "Set User Role for {0}" + +msgid "user.baseid.set.btn" +msgstr "Set Base-ID" + +msgid "user.del.fail.1" +msgstr "You can't delete yourself." + +msgid "user.del.fail.2" +msgstr "Deleting the user failed." + +msgid "user.del.fail.2" +msgstr "Please confirm that you want to delete the user." + +msgid "user.del.exp.1" +msgstr "Delete user \"{0}\"" + +msgid "user.del.exp.2" +msgstr "The profile data will be deleted." + +msgid "user.del.exp.3" +msgstr "All uploads will be deleted." + +msgid "user.del.exp.4" +msgstr "Existing postings will remain but shown with an unknown user." + +msgid "user.del.confirm" +msgstr "Delete the user. This can't be undone." + +msgid "user.del.btn.t" +msgstr "Delete user" + +msgid "user.del.ok.m" +msgstr "User \"{0}\" was deleted." + #, fuzzy #~ msgid "Uploads" #~ msgstr "Upload" @@ -1669,6 +1675,3 @@ msgstr "" #~ "config/email.php.default as template. See " #~ "also." - -#~ msgid "block_user_ui" -#~ msgstr "Lock Users" diff --git a/src/Locale/en/nondynamic.po b/src/Locale/en/nondynamic.po index e28b1fdb6..03187b25c 100644 --- a/src/Locale/en/nondynamic.po +++ b/src/Locale/en/nondynamic.po @@ -147,14 +147,6 @@ msgstr "System Address" msgid "email_system_exp" msgstr "Used as sender address for system generated messages (e.g. notifications). (optional, if empty main address is used)" -# -msgid "block_user_ui" -msgstr "Lock Users" - -# -msgid "block_user_ui_exp" -msgstr "Moderators are allowed to block users." - # msgid "store_ip" msgstr "Store IP" @@ -381,4 +373,21 @@ msgstr "Unhappy" msgid "smilies.t.wink" msgstr "Wink" +#******************************************************************************* +#* Permission roles +#******************************************************************************/ + +msgid "permission.role.0" +msgstr "Anonymous" + +msgid "permission.role.1" +msgstr "User" + +msgid "permission.role.2" +msgstr "Moderator" + +msgid "permission.role.3" +msgstr "Administrator" +msgid "permission.role.4" +msgstr "Owner" diff --git a/src/Model/Behavior/PostingBehavior.php b/src/Model/Behavior/PostingBehavior.php index 7a600cf40..34869e91f 100644 --- a/src/Model/Behavior/PostingBehavior.php +++ b/src/Model/Behavior/PostingBehavior.php @@ -15,10 +15,17 @@ use App\Lib\Model\Table\FieldFilter; use App\Model\Entity\Entry; use App\Model\Table\EntriesTable; +use Cake\Cache\Cache; +use Cake\Datasource\Exception\RecordNotFoundException; use Cake\ORM\Behavior; +use Cake\ORM\Query; use Saito\Posting\Basic\BasicPostingInterface; use Saito\Posting\Posting; +use Saito\Posting\PostingInterface; +use Saito\Posting\TreeBuilder; use Saito\User\CurrentUser\CurrentUserInterface; +use Stopwatch\Lib\Stopwatch; +use Traversable; class PostingBehavior extends Behavior { @@ -90,9 +97,9 @@ public function updatePosting(Entry $posting, array $data, CurrentUserInterface { $data = $this->fieldFilter->filterFields($data, 'update'); $isRoot = $posting->isRoot(); - $parent = $this->getTable()->get($posting->get('pid')); if (!$isRoot) { + $parent = $this->getTable()->get($posting->get('pid')); $data = $this->prepareChildPosting($parent, $data); } @@ -101,6 +108,7 @@ public function updatePosting(Entry $posting, array $data, CurrentUserInterface /// must be set for validation $data['locked'] = $posting->get('locked'); $data['fixed'] = $posting->get('fixed'); + $data['category_id'] = $data['category_id'] ?? $posting->get('category_id'); $data['pid'] = $posting->get('pid'); $data['time'] = $posting->get('time'); @@ -133,7 +141,7 @@ public function prepareChildPosting(BasicPostingInterface $parent, array $data): $data['subject'] = $parent->get('subject'); } - $data['category_id'] = $parent->get('category_id'); + $data['category_id'] = $data['category_id'] ?? $parent->get('category_id'); $data['tid'] = $parent->get('tid'); return $data; @@ -181,8 +189,223 @@ public function validateCategoryIsAllowed($categoryId, $context): bool */ public function validateEditingAllowed($check, $context): bool { - $posting = new Posting($this->CurrentUser, $context['data']); + $posting = (new Posting($context['data']))->withCurrentUser($this->CurrentUser); return $posting->isEditingAllowed(); } + + /** + * Get an array of postings for threads + * + * @param array $tids Thread-IDs + * @param array|null $order Thread sort order + * @param CurrentUserInterface $CU Current User + * @return array Array of postings found + * @throws RecordNotFoundException If no thread is found + */ + public function postingsForThreads(array $tids, ?array $order = null, CurrentUserInterface $CU = null): array + { + $entries = $this->getTable() + ->find('entriesForThreads', ['threadOrder' => $order, 'tids' => $tids]) + ->all(); + + if (!count($entries)) { + throw new RecordNotFoundException( + sprintf('No postings for thread-IDs "%s".', implode(', ', $tids)) + ); + } + + return $this->entriesToPostings($entries, $CU); + } + + /** + * Get a posting for a thread + * + * @param int $tid Thread-ID + * @param bool $complete complete fieldset + * @param CurrentUserInterface|null $CurrentUser CurrentUser + * @return PostingInterface + * @throws RecordNotFoundException If thread isn't found + */ + public function postingsForThread(int $tid, bool $complete = false, ?CurrentUserInterface $CurrentUser = null): PostingInterface + { + $entries = $this->getTable() + ->find('entriesForThreads', ['complete' => $complete, 'tids' => [$tid]]) + ->all(); + + if (!count($entries)) { + throw new RecordNotFoundException( + sprintf('No postings for thread-ID "%s".', $tid) + ); + } + + $postings = $this->entriesToPostings($entries, $CurrentUser); + + return reset($postings); + } + + /** + * Delete a node + * + * @param int $id the node id + * @return bool + */ + public function deletePosting(int $id): bool + { + $root = $this->postingsForNode($id); + if (empty($root)) { + throw new \InvalidArgumentException(); + } + + $nodesToDelete[] = $root; + $nodesToDelete = array_merge($nodesToDelete, $root->getAllChildren()); + + $idsToDelete = []; + foreach ($nodesToDelete as $node) { + $idsToDelete[] = $node->get('id'); + }; + + /** @var EntriesTable */ + $table = $this->getTable(); + + return $table->deleteWithIds($idsToDelete); + } + + /** + * Get recent postings + * + * ### Options: + * + * - `user_id` int| If provided finds only postings of that user. + * - `limit` int <10> Number of postings to find. + * + * @param CurrentUserInterface $User User who has access to postings + * @param array $options find options + * + * @return array Array of Postings + */ + public function getRecentPostings(CurrentUserInterface $User, array $options = []): array + { + Stopwatch::start('PostingBehavior::getRecentPostings'); + + $options += [ + 'user_id' => null, + 'limit' => 10, + ]; + + $options['category_id'] = $User->getCategories()->getAll('read'); + + $read = function () use ($options) { + $conditions = []; + if ($options['user_id'] !== null) { + $conditions[]['Entries.user_id'] = $options['user_id']; + } + if ($options['category_id'] !== null) { + $conditions[]['Entries.category_id IN'] = $options['category_id']; + }; + + $result = $this + ->getTable() + ->find( + 'entry', + [ + 'conditions' => $conditions, + 'limit' => $options['limit'], + 'order' => ['time' => 'DESC'] + ] + ) + // hydrating kills performance + ->enableHydration(false) + ->all(); + + return $result; + }; + + $key = 'Entry.recentEntries-' . md5(serialize($options)); + $results = Cache::remember($key, $read, 'entries'); + + $threads = []; + foreach ($results as $result) { + $threads[$result['id']] = (new Posting($result))->withCurrentUser($User); + } + + Stopwatch::stop('PostingBehavior::getRecentPostings'); + + return $threads; + } + + /** + * Convert array with Entry entities to array with Postings + * + * @param Traversable $entries Entry array + * @param CurrentUserInterface|null $CurrentUser The current user + * @return array + */ + protected function entriesToPostings(Traversable $entries, ?CurrentUserInterface $CurrentUser = null): array + { + Stopwatch::start('PostingBehavior::entriesToPostings'); + $threads = []; + $postings = (new TreeBuilder())->build($entries); + foreach ($postings as $thread) { + $posting = new Posting($thread); + if ($CurrentUser) { + $posting->withCurrentUser($CurrentUser); + } + $threads[$thread['tid']] = $posting; + } + Stopwatch::stop('PostingBehavior::entriesToPostings'); + + return $threads; + } + + /** + * tree of a single node and its subentries + * + * @param int $id id + * @return PostingInterface|null tree or null if nothing found + */ + protected function postingsForNode(int $id) : ?PostingInterface + { + /** @var EntriesTable */ + $table = $this->getTable(); + $tid = $table->getThreadId($id); + $postings = $this->postingsForThreads([$tid]); + $postings = array_shift($postings); + + return $postings->getThread()->get($id); + } + + /** + * Finder to get all entries for threads + * + * @param Query $query Query + * @param array $options Options + * - 'tids' array required thread-IDs + * - 'complete' fieldset + * - 'threadOrder' order + * @return Query + */ + public function findEntriesForThreads(Query $query, array $options): Query + { + Stopwatch::start('PostingBehavior::findEntriesForThreads'); + $options += [ + 'complete' => false, + 'threadOrder' => ['last_answer' => 'ASC'], + ]; + if (empty($options['tids'])) { + throw new \InvalidArgumentException('Not threads to find.'); + } + $tids = $options['tids']; + $order = $options['threadOrder']; + unset($options['threadOrder'], $options['tids']); + + $query = $query->find('entry', $options) + ->where(['tid IN' => $tids]) + ->order($order) + // hydrating kills performance + ->enableHydration(false); + Stopwatch::stop('PostingBehavior::findEntriesForThreads'); + + return $query; + } } diff --git a/src/Model/Entity/Entry.php b/src/Model/Entity/Entry.php index 3c3afa910..c53462e40 100644 --- a/src/Model/Entity/Entry.php +++ b/src/Model/Entity/Entry.php @@ -16,6 +16,7 @@ use Saito\App\Registry; use Saito\Posting\Basic\BasicPostingInterface; use Saito\Posting\Basic\BasicPostingTrait; +use Saito\Posting\Posting; use Saito\Posting\PostingInterface; class Entry extends Entity implements BasicPostingInterface @@ -49,12 +50,6 @@ public function _setText(?string $text) */ public function toPosting(): PostingInterface { - /** @var PostingInterface */ - $posting = Registry::newInstance( - '\Saito\Posting\Posting', - ['rawData' => $this->toArray()] - ); - - return $posting; + return new Posting($this->toArray()); } } diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php index df2a685e5..409be4f85 100644 --- a/src/Model/Entity/User.php +++ b/src/Model/Entity/User.php @@ -13,45 +13,10 @@ namespace App\Model\Entity; use Cake\ORM\Entity; -use Saito\User\SaitoUser; +use Saito\User\ForumsUserInterface; +use Saito\User\ForumsUserTrait; -/** - * @method string getRole() - */ -class User extends Entity +class User extends Entity implements ForumsUserInterface { - /** - * {@inheritDoc} - * - * Make ForumsUserInterface available. - */ - public function __call($method, $arguments) - { - $suser = $this->toSaitoUser(); - if (is_callable([$suser, $method])) { - return call_user_func_array([$suser, $method], $arguments); - } - $class = get_class($this); - throw new \Exception("Invalid method {$class}::{$method}()"); - } - - /** - * Return user as SaitoUser - * - * @return SaitoUser - */ - public function toSaitoUser() - { - return new SaitoUser($this->toArray()); - } - - /** - * Get number of postings - * - * @return mixed - */ - public function numberOfPostings() - { - return $this->get('entry_count'); - } + use ForumsUserTrait; } diff --git a/src/Model/Table/EntriesTable.php b/src/Model/Table/EntriesTable.php index a6e37e3a5..a7eee7a09 100644 --- a/src/Model/Table/EntriesTable.php +++ b/src/Model/Table/EntriesTable.php @@ -17,20 +17,17 @@ use App\Model\Table\CategoriesTable; use App\Model\Table\DraftsTable; use Bookmarks\Model\Table\BookmarksTable; -use Cake\Cache\Cache; +use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\Event; use Cake\Http\Exception\NotFoundException; use Cake\ORM\Entity; use Cake\ORM\Query; use Cake\ORM\RulesChecker; use Cake\Validation\Validator; -use Saito\App\Registry; -use Saito\Posting\Posting; -use Saito\RememberTrait; +use Saito\Posting\PostingInterface; use Saito\User\CurrentUser\CurrentUserInterface; use Saito\Validation\SaitoValidationProvider; use Search\Manager; -use Stopwatch\Lib\Stopwatch; /** * Stores postings @@ -46,11 +43,13 @@ * @method createPosting(array $data, CurrentUserInterface $CurrentUser) * @method updatePosting(Entry $posting, array $data, CurrentUserInterface $CurrentUser) * @method array prepareChildPosting(BasicPostingInterface $parent, array $data) + * @method array getRecentPostings(CurrentUserInterface $CU, ?array $options = []) + * @method bool deletePosting(int $id) + * @method array postingsForThreads(array $tids, ?array $order = null, ?CurrentUserInterface $CU) + * @method PostingInterface postingsForThread(int $tid, ?bool $complete = false, ?CurrentUserInterface $CU) */ class EntriesTable extends AppTable { - use RememberTrait; - /** * Max subject length. * @@ -70,50 +69,6 @@ class EntriesTable extends AppTable 'category' => ['type' => 'value'], ]; - /** - * field list necessary for displaying a thread_line - * - * Entry.text determine if Entry is n/t - * - * @var array - */ - public $threadLineFieldList = [ - 'Entries.id', - 'Entries.pid', - 'Entries.tid', - 'Entries.subject', - 'Entries.text', - 'Entries.time', - 'Entries.fixed', - 'Entries.last_answer', - 'Entries.views', - 'Entries.user_id', - 'Entries.locked', - 'Entries.name', - 'Entries.solves', - 'Users.username', - 'Categories.id', - 'Categories.accession', - 'Categories.category', - 'Categories.description' - ]; - - /** - * fields additional to $threadLineFieldList to show complete entry - * - * @var array - */ - public $showEntryFieldListAdditional = [ - 'Entries.edited', - 'Entries.edited_by', - 'Entries.ip', - 'Entries.category_id', - 'Users.id', - 'Users.avatar', - 'Users.signature', - 'Users.user_place' - ]; - protected $_defaultConfig = [ 'subject_maxlength' => 100 ]; @@ -128,7 +83,6 @@ public function initialize(array $config) $this->addBehavior('Posting'); $this->addBehavior('IpLogging'); $this->addBehavior('Timestamp'); - $this->addBehavior('Tree'); $this->addBehavior( 'CounterCache', @@ -307,144 +261,114 @@ public function searchManager(): Manager } /** - * Get recent postings + * Shorthand for reading an entry with full da516ta * - * ### Options: - * - * - `user_id` int| If provided finds only postings of that user. - * - `limit` int <10> Number of postings to find. - * - * @param CurrentUserInterface $User User who has access to postings - * @param array $options find options - * - * @return array Array of Postings + * @param int $primaryKey key + * @param array $options options + * @return mixed Posting if found false otherwise */ - public function getRecentEntries( - CurrentUserInterface $User, - array $options = [] - ) { - Stopwatch::start('Model->User->getRecentEntries()'); - - $options += [ - 'user_id' => null, - 'limit' => 10, - ]; - - $options['category_id'] = $User->getCategories()->getAll('read'); - - $read = function () use ($options) { - $conditions = []; - if ($options['user_id'] !== null) { - $conditions[]['Entries.user_id'] = $options['user_id']; - } - if ($options['category_id'] !== null) { - $conditions[]['Entries.category_id IN'] = $options['category_id']; - }; - - $result = $this - ->find( - 'all', - [ - 'contain' => ['Users', 'Categories'], - 'fields' => $this->threadLineFieldList, - 'conditions' => $conditions, - 'limit' => $options['limit'], - 'order' => ['time' => 'DESC'] - ] - ) - // hydrating kills performance - ->enableHydration(false) - ->all(); - - return $result; - }; - - $key = 'Entry.recentEntries-' . md5(serialize($options)); - $results = Cache::remember($key, $read, 'entries'); - - $threads = []; - foreach ($results as $result) { - $threads[$result['id']] = Registry::newInstance( - '\Saito\Posting\Posting', - ['rawData' => $result] - ); - } - - Stopwatch::stop('Model->User->getRecentEntries()'); + public function get($primaryKey, $options = []) + { + /** @var Entry */ + $result = $this->find('entry', ['complete' => true]) + ->where([$this->getAlias() . '.id' => $primaryKey]) + ->first(); - return $threads; + // @td throw exception here + return empty($result) ? false : $result; } /** - * Finds the thread-id for a posting + * Implements the custom find type 'entry' * - * @param int $id Posting-Id - * @return int Thread-Id - * @throws \UnexpectedValueException + * @param Query $query query + * @param array $options options + * - 'complete' bool controls fieldset selected as in getFieldset($complete) + * @return Query */ - public function getThreadId($id) + public function findEntry(Query $query, array $options = []) { - $entry = $this->find( - 'all', - ['conditions' => ['id' => $id], 'fields' => 'tid'] - )->first(); - if (empty($entry)) { - throw new \UnexpectedValueException( - 'Posting not found. Posting-Id: ' . $id - ); - } + $options += ['complete' => false]; + $query + ->select($this->getFieldset($options['complete'])) + ->contain(['Users', 'Categories']); - return $entry->get('tid'); + return $query; } /** - * Shorthand for reading an entry with full data + * Get list of fields required to display posting.:w * - * @param int $primaryKey key - * @param array $options options - * @return mixed Posting if found false otherwise + * You don't want to fetch every field for performance reasons. + * + * @param bool $complete Threadline if false; Full posting if true + * @return array The fieldset */ - public function get($primaryKey, $options = []) + public function getFieldset(bool $complete = false): array { - $options += ['return' => 'Posting']; - $return = $options['return']; - unset($options['return']); + // field list necessary for displaying a thread_line + $threadLineFieldList = [ + 'Categories.accession', + 'Categories.category', + 'Categories.description', + 'Categories.id', + 'Entries.fixed', + 'Entries.id', + 'Entries.last_answer', + 'Entries.locked', + 'Entries.name', + 'Entries.pid', + 'Entries.solves', + 'Entries.subject', + // Entry.text determines if Entry is n/t + 'Entries.text', + 'Entries.tid', + 'Entries.time', + 'Entries.user_id', + 'Entries.views', + 'Users.username', + ]; - /** @var Entry */ - $result = $this->find('entry') - ->where([$this->getAlias() . '.id' => $primaryKey]) - ->first(); + // fields additional to $threadLineFieldList to show complete entry + $showEntryFieldListAdditional = [ + 'Entries.category_id', + 'Entries.edited', + 'Entries.edited_by', + 'Entries.ip', + 'Users.avatar', + 'Users.id', + 'Users.signature', + 'Users.user_place' + ]; - if (!$result) { - return false; + $fields = $threadLineFieldList; + if ($complete) { + $fields = array_merge($fields, $showEntryFieldListAdditional); } - switch ($return) { - case 'Posting': - return $result->toPosting(); - case 'Entity': - default: - return $result; - } + return $fields; } /** - * get parent id + * Finds the thread-IT for a posting. * - * @param int $id id - * @return mixed - * @throws \UnexpectedValueException + * @param int $id Posting-Id + * @return int Thread-Id + * @throws RecordNotFoundException If posting isn't found */ - public function getParentId($id) + public function getThreadId($id) { - $entry = $this->find()->select('pid')->where(['id' => $id])->first(); - if (!$entry) { - throw new \UnexpectedValueException( + $entry = $this->find( + 'all', + ['conditions' => ['id' => $id], 'fields' => 'tid'] + )->first(); + if (empty($entry)) { + throw new RecordNotFoundException( 'Posting not found. Posting-Id: ' . $id ); } - return $entry->get('pid'); + return $entry->get('tid'); } /** @@ -523,7 +447,7 @@ public function updateEntry(Entry $posting, array $data): ?Entry /** @var Entry */ $new = $this->save($posting); - if (!$new) { + if (empty($new)) { return null; } @@ -535,135 +459,6 @@ public function updateEntry(Entry $posting, array $data): ?Entry return $new; } - /** - * tree of a single node and its subentries - * - * $options = array( - * 'root' => true // performance improvements if it's a known thread-root - * ); - * - * @param int $id id - * @param array $options options - * @return Posting|null tree or null if nothing found - */ - public function treeForNode(int $id, ?array $options = []): ?Posting - { - $options += [ - 'root' => false, - 'complete' => false - ]; - - if ($options['root']) { - $tid = $id; - } else { - $tid = $this->getThreadId($id); - } - - $fields = null; - if ($options['complete']) { - $fields = array_merge( - $this->threadLineFieldList, - $this->showEntryFieldListAdditional - ); - } - - $tree = $this->treesForThreads([$tid], null, $fields); - - if (!$tree) { - return null; - } - - $tree = reset($tree); - - //= extract subtree - if ((int)$tid !== (int)$id) { - $tree = $tree->getThread()->get($id); - } - - return $tree; - } - - /** - * trees for multiple tids - * - * @param array $ids ids - * @param array $order order - * @param array $fieldlist fieldlist - * @return array|null array of Postings, null if nothing found - */ - public function treesForThreads(array $ids, ?array $order = null, array $fieldlist = null): ?array - { - if (empty($ids)) { - return []; - } - - if (empty($order)) { - $order = ['last_answer' => 'ASC']; - } - - if ($fieldlist === null) { - $fieldlist = $this->threadLineFieldList; - } - - Stopwatch::start('EntriesTable::treesForThreads() DB'); - $postings = $this->_getThreadEntries( - $ids, - ['order' => $order, 'fields' => $fieldlist] - ); - Stopwatch::stop('EntriesTable::treesForThreads() DB'); - - if (!$postings->count()) { - return null; - } - - Stopwatch::start('EntriesTable::treesForThreads() CPU'); - $threads = []; - $postings = $this->treeBuild($postings); - foreach ($postings as $thread) { - $id = $thread['tid']; - $threads[$id] = $thread; - $threads[$id] = Registry::newInstance( - '\Saito\Posting\Posting', - ['rawData' => $thread] - ); - } - Stopwatch::stop('EntriesTable::treesForThreads() CPU'); - - return $threads; - } - - /** - * Returns all entries of threads $tid - * - * @param array $tid ids - * @param array $params params - * - 'fields' array of thread-ids: [1, 2, 5] - * - 'order' sort order for threads ['time' => 'ASC'], - * @return mixed unhydrated result set - */ - protected function _getThreadEntries(array $tid, array $params = []) - { - $params += [ - 'fields' => $this->threadLineFieldList, - 'order' => ['last_answer' => 'ASC'] - ]; - - $threads = $this - ->find( - 'all', - [ - 'conditions' => ['tid IN' => $tid], - 'contain' => ['Users', 'Categories'], - 'fields' => $params['fields'], - 'order' => $params['order'] - ] - ) - // hydrating kills performance - ->enableHydration(false); - - return $threads; - } - /** * Marks a sub-entry as solution to a root entry * @@ -730,38 +525,23 @@ public function beforeMarshal(Event $event, \ArrayObject $data, \ArrayObject $op /** * Deletes posting incl. all its subposting and associated data * - * @param int $id id - * @throws \InvalidArgumentException - * @throws \Exception + * @param array $idsToDelete Entry ids which should be deleted * @return bool */ - public function treeDeleteNode($id) + public function deleteWithIds(array $idsToDelete): bool { - $root = $this->treeForNode((int)$id); - - if (empty($root)) { - throw new \Exception; - } - - $nodesToDelete[] = $root; - $nodesToDelete = array_merge($nodesToDelete, $root->getAllChildren()); - - $idsToDelete = []; - foreach ($nodesToDelete as $node) { - $idsToDelete[] = $node->get('id'); - }; - $success = $this->deleteAll(['id IN' => $idsToDelete]); if (!$success) { return false; } + // @td Should be covered by dependent assoc. Add tests. $this->Bookmarks->deleteAll(['entry_id IN' => $idsToDelete]); $this->dispatchSaitoEvent( - 'Model.Saito.Posting.delete', - ['subject' => $root, 'table' => $this] + 'Model.Saito.Postings.delete', + ['subject' => $idsToDelete, 'table' => $this] ); return true; @@ -872,23 +652,6 @@ public function threadMerge($sourceId, $targetId) return false; } - /** - * Implements the custom find type 'entry' - * - * @param Query $query query - * @return Query - */ - public function findEntry(Query $query) - { - $fields = array_merge( - $this->threadLineFieldList, - $this->showEntryFieldListAdditional - ); - $query->select($fields)->contain(['Users', 'Categories']); - - return $query; - } - /** * Implements the custom find type 'index paginator' * diff --git a/src/Model/Table/SettingsTable.php b/src/Model/Table/SettingsTable.php index 92fbb807b..7dceb7fda 100755 --- a/src/Model/Table/SettingsTable.php +++ b/src/Model/Table/SettingsTable.php @@ -83,17 +83,20 @@ public function validationDefault(Validator $validator) */ public function getSettings() { - $settings = $this->find(); + $settings = $this->find()->all(); if (empty($settings)) { throw new \RuntimeException( 'No settings found in settings table.' ); } - $settings = $this->_compactKeyValue($settings); + $compact = []; + foreach ($settings as $result) { + $compact[$result->get('name')] = $result->get('value'); + } - $this->_fillOptionalEmailAddresses($settings); + $this->_fillOptionalEmailAddresses($compact); - return $settings; + return $compact; } /** @@ -146,25 +149,6 @@ public function validateSubjectMaxLength($value, array $context) return true; } - /** - * Returns a key-value array - * - * Fast version of Set::combine($results, '{n}.Setting.name', - * '{n}.Setting.value'); - * - * @param array $results results - * @return array - */ - protected function _compactKeyValue($results) - { - $settings = []; - foreach ($results as $result) { - $settings[$result->get('name')] = $result->get('value'); - } - - return $settings; - } - /** * Defaults optional email addresses to main address * diff --git a/src/Model/Table/UsersTable.php b/src/Model/Table/UsersTable.php index ffa577b1c..df328f728 100644 --- a/src/Model/Table/UsersTable.php +++ b/src/Model/Table/UsersTable.php @@ -26,12 +26,13 @@ use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\Event; use Cake\ORM\Entity; -use Cake\ORM\Locator\TableLocator; use Cake\ORM\Query; use Cake\ORM\TableRegistry; use Cake\Validation\Validation; use Cake\Validation\Validator; use DateTimeInterface; +use Saito\App\Registry; +use Saito\User\Permission\Permissions; use Saito\User\Upload\AvatarFilenameListener; use Stopwatch\Lib\Stopwatch; @@ -289,7 +290,7 @@ public function validationDefault(Validator $validator) 'user_type', [ 'allowedType' => [ - 'rule' => ['inList', ['user', 'mod', 'admin']] + 'rule' => [$this, 'validateUserRoleExists'], ] ] ); @@ -452,11 +453,8 @@ public function userlist() */ public function deleteAllExceptEntries(int $userId) { - if ($userId == 1) { - return false; - } $user = $this->get($userId); - if (!$user) { + if (empty($user)) { return false; } @@ -605,6 +603,25 @@ public function validateHasAllowedChars($value, array $context) return true; } + /** + * Check if the role exists + * + * @param string $value value + * @param array $context context + * @return bool|string + */ + public function validateUserRoleExists($value, array $context) + { + /** @var Permissions */ + $Permissions = Registry::get('Permissions'); + $roles = array_column($Permissions->getRoles()->getAvailable(), 'type'); + if (in_array($value, $roles)) { + return true; + } + + return __('vld.user.user_type.allowedType', h($value)); + } + /** * checks if equal username exists * @@ -717,12 +734,8 @@ public function userBlockGc() * user data on success * @throws \InvalidArgumentException */ - public function activate($userId, $code) + public function activate(int $userId, string $code) { - if (!is_int($userId) || !is_string($code)) { - throw new \InvalidArgumentException(); - } - try { $user = $this->get($userId); } catch (RecordNotFoundException $e) { diff --git a/src/Shell/SaitoDummyDataShell.php b/src/Shell/SaitoDummyDataShell.php index 41e0ce8a1..fa39ee1f7 100644 --- a/src/Shell/SaitoDummyDataShell.php +++ b/src/Shell/SaitoDummyDataShell.php @@ -129,12 +129,12 @@ public function generatePostings() $nPostings = (int)$this->in( 'Number of postings to generate?', null, - 100 + '100' ); if ($nPostings === 0) { return; } - $ratio = (int)$this->in('Average answers per thread?', null, 10); + $ratio = (int)$this->in('Average answers per thread?', null, '10'); $seed = $nPostings / $ratio; for ($i = 0; $i < $nPostings; $i++) { @@ -150,7 +150,6 @@ public function generatePostings() $posting['pid'] = array_rand($this->_Threads, 1); } $user = $this->_randomUser(); - Registry::set('CU', $user); $posting = $this->Entries->createPosting($posting, $user); @@ -181,7 +180,7 @@ public function generateUsers() $n = (int)$this->in( "Number of users to generate (max: $max)?", null, - 0 + '0' ); if ($n === 0) { return; diff --git a/src/Template/Cell/SlidetabUserlist/display.ctp b/src/Template/Cell/SlidetabUserlist/display.ctp index a82fe49c5..ced449cd5 100644 --- a/src/Template/Cell/SlidetabUserlist/display.ctp +++ b/src/Template/Cell/SlidetabUserlist/display.ctp @@ -34,15 +34,19 @@ class="get('id') == $CurrentUser->getId()) ? 'slidetab-actUser' : '' ?>"> getRole(); - if ($role === 'admin') { - $title = __('user.type.admin'); - $icon = 'fa-admin'; - } elseif ($role === 'mod') { - $title = __('user.type.mod'); - $icon = 'fa-mod'; - } else { - $title = __('user.type.user'); - $icon = 'fa-user'; + $title = $this->Permissions->roleAsString($role); + switch ($role) { + case ('owner'): + $icon = 'fa-star-o'; + break; + case ('admin'): + $icon = 'fa-admin'; + break; + case ('mod'): + $icon = 'fa-mod'; + break; + default: + $icon = 'fa-user'; } ?> diff --git a/src/Template/Element/entry/view_content.ctp b/src/Template/Element/entry/view_content.ctp index dc788b18b..fc1bc12ec 100644 --- a/src/Template/Element/entry/view_content.ctp +++ b/src/Template/Element/entry/view_content.ctp @@ -80,7 +80,7 @@ $schemaMeta = []; } $schemaMeta['interactionCount'] = "UserPageVisits:{$entry->get('views')}"; - if (Configure::read('Saito.Settings.store_ip') && $CurrentUser->permission('saito.core.view.ip')) { + if (Configure::read('Saito.Settings.store_ip') && $CurrentUser->permission('saito.core.posting.ip.view')) { echo ', IP: ' . $entry->get('ip'); } diff --git a/src/Template/Element/entry/view_posting.ctp b/src/Template/Element/entry/view_posting.ctp index 293311fdb..a1b9c673c 100644 --- a/src/Template/Element/entry/view_posting.ctp +++ b/src/Template/Element/entry/view_posting.ctp @@ -96,21 +96,22 @@ $jsEntry = json_encode( // edit entry $editLinkIsShown = true; $menuItems[] = $this->Html->link( - ' ' . __('edit_linkname'), + ' ' . __('edit_linkname'), '/entries/edit/' . $entry->get('id'), ['class' => 'dropdown-item', 'escape' => false] ); } - if ($CurrentUser->permission('saito.core.posting.edit.restricted')) { - // pin and lock thread - if ($entry->isRoot()) { - if ($editLinkIsShown) { - $menuItems[] = 'divider'; - } + /// pin and lock thread + if ($entry->isRoot()) { + if (!empty($menuItems)) { + $menuItems[] = 'divider'; + } + + if ($CurrentUser->permission('saito.core.posting.pinAndLock')) { $ajaxToggleOptions = [ - 'fixed' => 'fa fa-thumb-tack', - 'locked' => 'fa fa-lock' + 'fixed' => 'fa fa-fw fa-thumb-tack', + 'locked' => 'fa fa-fw fa-lock' ]; foreach ($ajaxToggleOptions as $key => $icon) { if (($entry->get($key) == 0)) { @@ -131,23 +132,28 @@ $jsEntry = json_encode( $options ); } + } - $menuItems[] = 'divider'; - + if ($CurrentUser->permission('saito.core.posting.merge')) { + if (!empty($menuItems)) { + $menuItems[] = 'divider'; + } // merge thread $menuItems[] = $this->Html->link( - ' ' . __( - 'merge_tree_link' - ), + ' ' . h(__('merge_tree_link')), '/entries/merge/' . $entry->get('id'), ['class' => 'dropdown-item', 'escape' => false] ); } + } + if ($CurrentUser->permission('saito.core.posting.delete')) { // delete - $menuItems[] = 'divider'; + if (!empty($menuItems)) { + $menuItems[] = 'divider'; + } $menuItems[] = $this->Html->link( - ' ' . __('delete_tree_link'), + ' ' . h(__('delete_tree_link')), '#', ['class' => 'dropdown-item js-delete', 'escape' => false] ); diff --git a/src/Template/Element/layout/disclaimer.ctp b/src/Template/Element/layout/disclaimer.ctp index e40fe58cf..e6dc44f72 100644 --- a/src/Template/Element/layout/disclaimer.ctp +++ b/src/Template/Element/layout/disclaimer.ctp @@ -23,7 +23,7 @@ Stopwatch::start('layout/disclaimer.ctp');

- cell('AppStatus') ?> + cell('AppStatus', ['CurrentUser' => $CurrentUser]) ?>

diff --git a/src/Template/Element/layout/header_login.ctp b/src/Template/Element/layout/header_login.ctp index d756c07a4..9b5ab2073 100644 --- a/src/Template/Element/layout/header_login.ctp +++ b/src/Template/Element/layout/header_login.ctp @@ -1,17 +1,19 @@ isLoggedIn()) { - $register = $this->request->getAttribute('webroot') . 'users/register/'; - echo ''; - echo __('register_linkname'); - echo ''; + if ($CurrentUser->permission('saito.core.user.register')) { + $register = $this->request->getAttribute('webroot') . 'users/register/'; + echo ''; + echo __('register_linkname'); + echo ''; + } $action = $this->request->getParam('action'); if ($action !== 'login' && $action !== 'register') { echo $this->element('users/login_modal'); ?> + id="showLoginForm" title="" + class='btn btn-link' rel="nofollow"> Layout->textWithIcon(__('login_btn'), 'sign-in') ?> '; foreach ($slidetabs as $slidetab) { - echo $this->cell($slidetab); + echo $this->cell($slidetab, ['CurrentUser' => $CurrentUser]); } echo ''; \Stopwatch\Lib\Stopwatch::end('Slidetabs'); diff --git a/src/Template/Users/baseid.ctp b/src/Template/Users/baseid.ctp new file mode 100644 index 000000000..43b0494aa --- /dev/null +++ b/src/Template/Users/baseid.ctp @@ -0,0 +1,42 @@ + +start('headerSubnavLeft'); +echo $this->Layout->navbarBack( + ['controller' => 'users', 'action' => 'view', $user->get('id')] +); +$this->end(); +?> +
+
+ Layout->panelHeading( + __('user.role.set.t', $user->get('username')), + ['pageHeading' => true] + ) +?> +
+
+ Form->create($user) ?> +
+ Form->control( + 'username', + ['class' => 'form-control', + 'label' => __('username_marking')] + ) ?> +
+
+ Form->control( + 'user_email', + ['class' => 'form-control', + 'label' => __('userlist_email')] + ) ?> +
+
+ Form->submit( + __('user.baseid.set.btn'), + ['class' => 'btn btn-primary'] + ) ?> +
+ Form->end() ?> +
+
diff --git a/src/Template/Users/edit.ctp b/src/Template/Users/edit.ctp index 00a62b23c..3bc411062 100644 --- a/src/Template/Users/edit.ctp +++ b/src/Template/Users/edit.ctp @@ -1,4 +1,7 @@ start('headerSubnavLeft'); echo $this->Layout->navbarBack( [ @@ -15,52 +18,43 @@ $this->end();

permission('saito.core.user.edit')) { - $cells = [ - [ - __('username_marking'), - $this->Form->control('username', ['class' => 'form-control', 'label' => false]) - ], - [ - __('userlist_email'), - $this->Form->control('user_email', ['class' => 'form-control', 'label' => false]) - ], - [ - __('user_type'), - $this->Form->control( - 'user_type', - [ - 'class' => 'ml-3 mr-1', - 'label' => false, - 'options' => [ - 'user' => __('user.type.user'), - 'mod' => __('user.type.mod'), - 'admin' => __('user.type.admin'), - ], - 'type' => 'radio', - ] - ) - ] + $cells = []; + + if ($CurrentUser->permission('saito.core.user.name.set', (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId()))) { + $cells[] = [ + __('username_marking'), + $this->Form->control('username', ['class' => 'form-control', 'label' => false]) + ]; + } else { + $cells[] = [__('username_marking'), h($user->get('username'))]; + } + + if ($CurrentUser->permission('saito.core.user.email.set', (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId()))) { + $cells[] = [ + __('userlist_email'), + $this->Form->control('user_email', ['class' => 'form-control', 'label' => false]) ]; + } else { + $cells[] = [__('userlist_email'), h($user->get('user_email'))]; + } - /// Change password option already exists if same user - if ($CurrentUser->getId() !== $user->get('id')) { - $cells[] = [ - __('user_pw'), + $idP = (new ResourceAI())->onRole($user->getRole()); + if ($CurrentUser->permission('saito.core.user.role.set.restricted', $idP) + || $CurrentUser->permission('saito.core.user.role.set.unrestricted', $idP) + ) { + $cells[] = [ + __('user_type'), + $this->Html->para(null, $this->Permissions->roleAsString($user->getRole())) . + $this->Html->para( + null, $this->Html->link( - __('user.pw.set.btn'), - [ - 'action' => 'setpassword', - $user->get('id') - ] + __('user.role.set.btn'), + ['action' => 'role', $user->get('id')] ) - ]; - } - } else { - $cells = [ - [__('username_marking'), h($user->get('username'))], - [__('userlist_email'), h($user->get('user_email'))] + ) ]; + } else { + $cells[] = [__('user_type'), $this->Permissions->roleAsString($user->getRole())]; } if ($user->isUser($CurrentUser)) { @@ -74,6 +68,14 @@ $this->end(); ] ) ]; + } elseif ($CurrentUser->permission('saito.core.user.password.set')) { + $cells[] = [ + __('user_pw'), + $this->Html->link( + __('user.pw.set.btn'), + ['action' => 'setpassword', $user->get('id')] + ), + ]; } $avatar = $this->User->getAvatar($user, ['link' => false]); diff --git a/src/Template/Users/index.ctp b/src/Template/Users/index.ctp index b285b5e7d..fc78c18ad 100755 --- a/src/Template/Users/index.ctp +++ b/src/Template/Users/index.ctp @@ -35,7 +35,7 @@ $this->element('users/menu'); User->type($user->get('user_type')), + $this->Permissions->roleAsString($user->getRole()), __( 'user_since {0}', $this->TimeH->formatTime( @@ -47,7 +47,7 @@ $this->element('users/menu'); if ($user->get('user_online') && $user->get('user_online')['logged_in']) { $u[] = __('Online'); } - if (!$user->isActivated() && $CurrentUser->permission('saito.core.user.activate')) { + if (!$user->isActivated() && $CurrentUser->permission('saito.core.user.activate.view')) { $u[] = h(__('user.actv.ny')); } if ($user->isLocked()) { diff --git a/src/Template/Users/role.ctp b/src/Template/Users/role.ctp new file mode 100644 index 000000000..49829a6fc --- /dev/null +++ b/src/Template/Users/role.ctp @@ -0,0 +1,41 @@ +start('headerSubnavLeft'); +echo $this->Layout->navbarBack( + ['controller' => 'users', 'action' => 'edit', $user->get('id')] +); +$this->end(); +?> +
+
+ Layout->panelHeading( + __('user.role.set.t', $user->get('username')), + ['pageHeading' => true] + ) +?> +
+
+ Form->create($user) ?> +
+ Form->control( + 'user_type', + [ + 'class' => 'ml-3 mr-1', + 'label' => false, + 'options' => array_map(function ($role) { + return ['text' => $this->Permissions->roleAsString($role), 'value' => $role]; + }, $roles), + 'required' => true, + 'type' => 'radio', + ] + ) ?> +
+
+ Form->submit( + __('user.role.set.btn'), + ['class' => 'btn btn-primary'] + ) ?> +
+ Form->end() ?> +
+
diff --git a/src/Template/Users/view.ctp b/src/Template/Users/view.ctp index 204bbcd12..17939bdb0 100644 --- a/src/Template/Users/view.ctp +++ b/src/Template/Users/view.ctp @@ -1,6 +1,6 @@ start('headerSubnavLeft'); echo $this->Layout->navbarBack(); @@ -14,13 +14,11 @@ $urlToHistory = [ '?' => ['name' => $user->get('username')] ]; +$role = $this->Permissions->roleAsString($user->getRole()); $table = [ [ __('username_marking'), - h( - $user->get('username') - ) . " ({$this->User->type($user->get('user_type'))})", - # @td user_type for mod and admin + h($user->get('username')) . " ({$role})", ] ]; @@ -29,7 +27,7 @@ $table[] = [ $this->User->getAvatar($user, ['link' => false]) ]; -if (!$user->isActivated() && $CurrentUser->permission('saito.core.user.activate')) { +if (!$user->isActivated() && $CurrentUser->permission('saito.core.user.activate.view')) { $table[] = [ h(__('user.actv.t')), h(__('user.actv.ny')) @@ -172,11 +170,9 @@ if ($items) {
isLoggedIn(); - $isUsersEntry = $user->isUser($CurrentUser); - $panel = ''; - if ($isUsersEntry) { + + if ($CurrentUser->permission('saito.core.user.edit', (new ResourceAI())->onRole($user->getRole())->onOwner($user->getId()))) { $panel .= $this->Html->link( __('edit_userdata'), ['action' => 'edit', $user->get('id')], @@ -186,7 +182,8 @@ if ($items) { ] ); } - if ($isLoggedIn && !$isUsersEntry) { + if (!$CurrentUser->isUser($user)) { + // START User ignore if ($CurrentUser->ignores($user->get('id'))) { $panel .= $this->Form->postLink( $this->Layout->textWithIcon( @@ -216,39 +213,39 @@ if ($items) { ] ); } + // END User ignore + } - $menuItems = []; + // START Admin menu + $menuItems = []; - if ($CurrentUser->permission('saito.core.user.edit')) { - // edit user - $menuItems[] = $this->Html->link( - ' ' . __('Edit'), - ['action' => 'edit', $user->get('id')], - ['class' => 'dropdown-item', 'escape' => false] - ); + $deleteAllowed = !$CurrentUser->isUser($user) && $CurrentUser->permission('saito.core.user.delete', (new ResourceAI())->onRole($user->getRole())); + if ($deleteAllowed) { + if (!empty($menuItems)) { $menuItems[] = 'divider'; - - // delete user - $menuItems[] = $this->Html->link( - ' ' . h(__('Delete')), - [ - 'plugin' => 'admin', - 'controller' => 'Users', - 'action' => 'delete', - $user->get('id'), - ], - ['class' => 'dropdown-item', 'escape' => false] - ); - } - if ($menuItems) { - $panel .= $this->Layout->dropdownMenuButton( - $menuItems, - [ - 'class' => 'btn btn-link', - ] - ); } + ?> + + Html->link( + ' ' . h(__('Delete')), + '#', + [ + 'class' => 'dropdown-item', + 'escape' => false, + 'onclick' => "event.preventDefault(); $('#deleteUserModal').modal('show');", + ] + ); } + + if ($menuItems) { + $panel .= $this->Layout->dropdownMenuButton( + $menuItems, + ['class' => 'btn btn-link'] + ); + } + // END Admin menu if ($panel) { ?>
+ if ($CurrentUser->permission('saito.core.user.lock.set', (new ResourceAI())->onRole($user->getRole()))) { ?>
Layout->panelHeading(__('user.block.history')) ?> @@ -345,6 +342,65 @@ if ($items) { ?>
+ + + +