diff --git a/.gitignore b/.gitignore index 0d18ab0bdc..49d08ba71e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ favicon.* -.htconfig.php -.htpreconfig.php +/.htconfig.php +/.htpreconfig.php \#* *.log *.out diff --git a/CHANGELOG b/CHANGELOG index 6d908f0f45..4e88d864c4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +Version 2019.06 (UNRELEASED) (2019-06-?) + Friendica Core: + Update to the documentation [realkinetix] + Enhancements to the API [MrPetovan] + Fixed the notification order [JeroenED] + Fixed the timezone of Friendica logs [nupplaphil] + Fixed tag completion painfully slow [AlfredSK] + Fixed a regression in notifications [MrPetovan] + Fixed an issue with smilies and code blocks [MrPetovan] + General Code cleaning and restructuring [nupplaphil] + Added frio color scheme sharing [JeroenED] + Added syslog and stream Logger [nupplaphil] + Added collapsible panel for connector permission fields [MrPetovan] + + Closed Issues: + 6303, 6478, 6319, 6921, 6903 + Version 2019.03 (2019-03-22) Friendica Core: Update to the translation (CS, DE, EN-GB, EN-US, ES, FR, IT, PL, SV, ZH-CN) [translation teams] diff --git a/VERSION b/VERSION index 78b7dcf0ce..bd61e84f20 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2019.03 +2019.06-dev diff --git a/boot.php b/boot.php index 76672dc80c..7cc158a26d 100644 --- a/boot.php +++ b/boot.php @@ -31,7 +31,7 @@ use Friendica\Util\DateTimeFormat; define('FRIENDICA_PLATFORM', 'Friendica'); define('FRIENDICA_CODENAME', 'Dalmatian Bellflower'); -define('FRIENDICA_VERSION', '2019.03'); +define('FRIENDICA_VERSION', '2019.06-dev'); define('DFRN_PROTOCOL_VERSION', '2.23'); define('NEW_UPDATE_ROUTINE_VERSION', 1170); diff --git a/config/dbstructure.config.php b/config/dbstructure.config.php old mode 100644 new mode 100755 index 398f671b22..4c2afc7f07 --- a/config/dbstructure.config.php +++ b/config/dbstructure.config.php @@ -34,7 +34,7 @@ use Friendica\Database\DBA; if (!defined('DB_UPDATE_VERSION')) { - define('DB_UPDATE_VERSION', 1305); + define('DB_UPDATE_VERSION', 1308); } return [ @@ -1210,6 +1210,7 @@ return [ ], "indexes" => [ "PRIMARY" => ["tid"], + "term_type" => ["term(64)", "type"], "oid_otype_type_term" => ["oid", "otype", "type", "term(32)"], "uid_otype_type_term_global_created" => ["uid", "otype", "type", "term(32)", "global", "created"], "uid_otype_type_url" => ["uid", "otype", "type", "url(64)"], diff --git a/config/defaults.config.php b/config/defaults.config.php index a6f90f319e..0d9e55c7d9 100644 --- a/config/defaults.config.php +++ b/config/defaults.config.php @@ -214,6 +214,10 @@ return [ // If activated, all hashtags will point to the local server. 'local_tags' => false, + // logger_config (String) + // Sets the logging adapter of Friendica globally (monolog, syslog, stream) + 'logger_config' => 'stream', + // max_batch_queue (Integer) // Maximum number of batched queue items for a single contact before subsequent messages are discarded. 'max_batch_queue' => 1000, diff --git a/database.sql b/database.sql index 5b415b65d0..272d02de6c 100644 --- a/database.sql +++ b/database.sql @@ -1112,6 +1112,7 @@ CREATE TABLE IF NOT EXISTS `term` ( `global` boolean NOT NULL DEFAULT '0' COMMENT '', `uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id', PRIMARY KEY(`tid`), + INDEX `term_type` (`term`(64), `type`), INDEX `oid_otype_type_term` (`oid`,`otype`,`type`,`term`(32)), INDEX `uid_otype_type_term_global_created` (`uid`,`otype`,`type`,`term`(32),`global`,`created`), INDEX `uid_otype_type_url` (`uid`,`otype`,`type`,`url`(64)), diff --git a/doc/Addons.md b/doc/Addons.md index 29cf22bfe9..5d4be6db16 100644 --- a/doc/Addons.md +++ b/doc/Addons.md @@ -411,6 +411,30 @@ Hook data: visitor => array with the contact record of the visitor url => the query string +### jot_networks +Called when displaying the post permission screen. +Hook data is a list of form fields that need to be displayed along the ACL. +Form field array structure is: + +- **type**: `checkbox` or `select`. +- **field**: Standard field data structure to be used by `field_checkbox.tpl` and `field_select.tpl`. + +For `checkbox`, **field** is: + - [0] (String): Form field name; Mandatory. + - [1]: (String): Form field label; Optional, default is none. + - [2]: (Boolean): Whether the checkbox should be checked by default; Optional, default is false. + - [3]: (String): Additional help text; Optional, default is none. + - [4]: (String): Additional HTML attributes; Optional, default is none. + +For `select`, **field** is: + - [0] (String): Form field name; Mandatory. + - [1] (String): Form field label; Optional, default is none. + - [2] (Boolean): Default value to be selected by default; Optional, default is none. + - [3] (String): Additional help text; Optional, default is none. + - [4] (Array): Associative array of options. Item key is option value, item value is option label; Mandatory. + + + ## Complete list of hook callbacks Here is a complete list of all hook callbacks with file locations (as of 24-Sep-2018). Please see the source for details of any hooks not documented above. diff --git a/doc/Install.md b/doc/Install.md index 005db5b7f3..0d4faa10b1 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -35,6 +35,11 @@ Requirements * Installation into a top-level domain or sub-domain (without a directory/path component in the URL) is preferred. Directory paths will not be as convenient to use and have not been thoroughly tested. * If your hosting provider doesn't allow Unix shell access, you might have trouble getting everything to work. +Optional +--- + +* PHP ImageMagick extension (php-imagick) for animated GIF support. + Installation procedure --- diff --git a/doc/Quick-Start-guide.md b/doc/Quick-Start-guide.md index 5c88ffaa06..2725fe4f8c 100644 --- a/doc/Quick-Start-guide.md +++ b/doc/Quick-Start-guide.md @@ -3,17 +3,19 @@ If you're not already logged in, do so in the frame below. Once you've logged in (or if you are already logged in), you'll now be looking at your profile page. -This is a bit like your Facebook wall. +This is a bit like a Facebook wall. It's where all your status messgages are kept, and where your friends come to post on your wall. -To write your status, simply click in the box that says "share". -When you do this, the box will expand. -You can see some formatting options at the top such as Bold, Italics and Underline, as well as ways to add links and pictures. -At the bottom you'll find some more links. + +To write your status, simply click on the Pencil & Paper icon in the top right (in the Frio theme), or click in the box that says "share" (other themes). +When you do this, the posting dialog box will appear or the share box will expand. + +You can see some formatting options such as Bold, Italics and Underline, as well as ways to add links, pictures (dependent on the theme), and a paperclip icon to attach or embed content. You can use these to upload pictures and files from your computer, share websites with a bit of preview text, or embed video and audio files from elsewhere on the web. +With the Frio theme, the browser tab can be used to upload and post media from your account. You can also set your post location here. -Once you've finished writing your post, click on the padlock icon to select who can see it. -If you do not use the padlock icon, your post will be public. +Once you've finished writing your post, click on the padlock icon or permissions tab to select who can see it. +If you do not change anything, your post will be public. This means it will appear to anybody who views your profile, and in the community tab if your site has it enabled, as well as in the network tab of any of your contacts. Play around with this a bit, then when you're ready to move on, we'll take a look at the Network Tab diff --git a/doc/Text_editor.md b/doc/Text_editor.md index 3e38ca5f1e..07e1fa929e 100644 --- a/doc/Text_editor.md +++ b/doc/Text_editor.md @@ -11,71 +11,85 @@ Creating posts Here you can find an overview of the different ways to create and edit your post. -One click on "Share" text box on top of your Home or Network page, and the post editor shows up: +One click on the Pencil & Paper icon in the top right of your Home or Network page, or the "Share" text box, and the post editor shows up. +Below are examples of the post editor in 3 of Friendica's common themes:
-default editor -
Default post editor, with default Friendica theme (duepuntozero)
+frio editor +
Post editor, with the Frio (popular default) theme.
+
+

+
+vier editor +
Post editor, with the Vier theme.
+
+

+
+duepuntozero editor +
Post editor, with the Duepuntozero theme.
-Post title is optional, you can set it clicking on "Set title". +Post title is optional, you can set it by clicking on "Set title". -Posts can optionally be in one or more categories. Write categories name separated by a comma to file your new post. +Posts can optionally be in one or more categories. Write category names separated by a comma to file your new post. The Big Empty Textarea is where you write your new post. -You can simply enter your text there and click "Share" button, and your new post will be public on your profile page and shared to your contact. +You can simply enter your text there and click the "Share" button, and your new post will be public on your profile page and shared to your contact. If plain text is not so exciting to you, Friendica understands BBCode to spice up your posts: bold, italic, images, links, lists.. See [BBCode tags reference](help/BBCode) page to see all what you can do. -The icons under the text area are there to help you to write posts quickly: +The icons under the text area are there to help you to write posts quickly, but vary depending on the theme: -editor Upload a picture from your computer. The image will be uploaded and correct bbcode tag will be added to your post.* +With the Frio theme, the Underline, Italics and Bold buttons should be self-explanatory. + +editor Upload a picture from your computer. The image will be uploaded and correct bbcode tag will be added to your post.* In the Frio theme, use the Browser tab instead to Upload and/or attach content to your post.

-paper_clip Add files from your computer. Same as picture, but for generic attachment to the post.* +paper_clip This depends on the theme: For Frio, this is to attach remote content - put in a URL to embed in your post, including video or audio content. For other themes: Add files from your computer. Same as picture, but for generic attachment to the post.*

-chain Add a web address (url). Enter an url and Friendica will add to your post a link to the url and an excerpt from the web site, if possible. +chain Add a web address (url). Enter a URL and Friendica will add to your post a link to the url and an excerpt from the web site, if possible.

-video Add a video. Enter the url to a video (ogg) or to a video page on youtube or vimeo, and it will be embedded in your post with a preview. Friendica is using [HTML5](http://en.wikipedia.org/wiki/HTML5_video) for embedding content. Therefore, the supported files are depending on your browser and operating system (OS). Some filetypes are WebM, MP4 and OGG.* +video Add a video. Enter the url to a video (ogg) or to a video page on youtube or vimeo, and it will be embedded in your post with a preview. (In the Frio theme, this is done with the paperclip as mentioned above.) Friendica is using [HTML5](http://en.wikipedia.org/wiki/HTML5_video) for embedding content. Therefore, the supported files are depending on your browser and operating system (OS). Some filetypes are WebM, MP4 and OGG.*

-mic Add an audio. Same as video, but for audio. Depending on your browser and operation system MP3, OGG and AAC are supported. Additionally, you are able to add URLs from audiohosters like Soundcloud. +mic Add an audio. Same as video, but for audio. Depending on your browser and operation system MP3, OGG and AAC are supported. Additionally, you are able to add URLs from audiohosters like Soundcloud.

-globe Set your geographic location. This location will be added into a Google Maps search. That's why a note like "New York" or "10004" is already enough. +globe Or location Set your geographic location. This location will be added into a Google Maps search. That's why a note like "New York" or "10004" is already enough. +

+
+

-* how to [upload](help/FAQ#upload) files - -Those icons can change with themes. Some examples: +These icons can change depending on the theme. Some examples: - + + + + + + - + - - - - -
Darkbubble: Vier: vier.png 
Smoothly: darkbubble.png(inkl. smoothly, testbubble) 
Frost: frost.png  
Vier: vier.png(inkl. dispy)
-

 

+* how to [upload](help/FAQ#upload) files

 

-**lock icon The lock** +**lock icon The Lock / Permissions** -The last button, the Lock, is the most important feature in Friendica. If the lock is open, your post will be public, and will shows up on your profile page when strangers visit it. +In Frio, the Permissions tab, or in other themes, the Lock button, is the most important feature in Friendica. If the lock is open, your post will be public, and will show up on your profile page when strangers visit it. Click on it and the *Permission settings* window (aka "*Access Control Selector*" or "*ACL Selector*") pops up. There you can select who can see the post. @@ -95,4 +109,4 @@ Click again on "show" or "don't show" to switch it off. You can search for contacts or groups with the search box. -See also [Group and Privacy](help/Groups-and-Privacy) \ No newline at end of file +See also [Group and Privacy](help/Groups-and-Privacy) diff --git a/doc/img/friendica_editor.png b/doc/img/editor_dpzero.png similarity index 100% rename from doc/img/friendica_editor.png rename to doc/img/editor_dpzero.png diff --git a/doc/img/editor_frio.png b/doc/img/editor_frio.png index cb5ad0bcc6..d37081b833 100644 Binary files a/doc/img/editor_frio.png and b/doc/img/editor_frio.png differ diff --git a/doc/img/editor_vier.png b/doc/img/editor_vier.png index 217f67ac6f..5278b0ff12 100644 Binary files a/doc/img/editor_vier.png and b/doc/img/editor_vier.png differ diff --git a/doc/img/frio_location.png b/doc/img/frio_location.png new file mode 100644 index 0000000000..8850c80089 Binary files /dev/null and b/doc/img/frio_location.png differ diff --git a/doc/img/vier_icons.png b/doc/img/vier_icons.png new file mode 100644 index 0000000000..4dc8ae03f9 Binary files /dev/null and b/doc/img/vier_icons.png differ diff --git a/include/api.php b/include/api.php index b1ee607abb..7bdab84d23 100644 --- a/include/api.php +++ b/include/api.php @@ -932,7 +932,6 @@ function api_format_data($root_element, $type, $data) */ function api_account_verify_credentials($type) { - $a = \get_app(); if (api_user() === false) { @@ -954,13 +953,9 @@ function api_account_verify_credentials($type) // - Adding last status if (!$skip_status) { - $user_info["status"] = api_status_show("raw"); - if (isset($user_info["status"])) { - if (!is_array($user_info["status"]) || !count($user_info["status"])) { - unset($user_info["status"]); - } else { - unset($user_info["status"]["user"]); - } + $item = api_get_last_status($user_info['pid'], $user_info['uid']); + if ($item) { + $user_info['status'] = api_format_item($item, $type); } } @@ -1244,105 +1239,61 @@ function api_media_upload() api_register_func('api/media/upload', 'api_media_upload', true, API_METHOD_POST); /** - * - * @param string $type Return type (atom, rss, xml, json) - * + * @param string $type Return format (atom, rss, xml, json) * @param int $item_id - * @return array|string - * @throws BadRequestException - * @throws ImagickException - * @throws InternalServerErrorException - * @throws UnauthorizedException + * @return string + * @throws Exception */ -function api_status_show($type, $item_id = 0) +function api_status_show($type, $item_id) { - $a = \get_app(); + Logger::info(API_LOG_PREFIX . 'Start', ['action' => 'status_show', 'type' => $type, 'item_id' => $item_id]); - $user_info = api_get_user($a); + $status_info = []; - Logger::log('api_status_show: user_info: '.print_r($user_info, true), Logger::DEBUG); - - if (!empty($item_id)) { - // Get the item with the given id - $condition = ['id' => $item_id]; - } else { - // get last public wall message - $condition = ['owner-id' => $user_info['pid'], 'uid' => api_user(), - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT]]; + $item = api_get_item(['id' => $item_id]); + if ($item) { + $status_info = api_format_item($item, $type); } - if ($type == "raw") { - $condition['private'] = false; - } + Logger::info(API_LOG_PREFIX . 'End', ['action' => 'get_status', 'status_info' => $status_info]); - $lastwall = Item::selectFirst(Item::ITEM_FIELDLIST, $condition, ['order' => ['id' => true]]); + return api_format_data('statuses', $type, ['status' => $status_info]); +} - if (DBA::isResult($lastwall)) { - $in_reply_to = api_in_reply_to($lastwall); +/** + * Retrieves the last public status of the provided user info + * + * @param int $ownerId Public contact Id + * @param int $uid User Id + * @return array + * @throws Exception + */ +function api_get_last_status($ownerId, $uid) +{ + $condition = [ + 'owner-id' => $ownerId, + 'uid' => $uid, + 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], + 'private' => false + ]; - $converted = api_convert_item($lastwall); + $item = api_get_item($condition); - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } + return $item; +} - $status_info = [ - 'created_at' => api_date($lastwall['created']), - 'id' => intval($lastwall['id']), - 'id_str' => (string) $lastwall['id'], - 'text' => $converted["text"], - 'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'), - 'truncated' => false, - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - 'user' => $user_info, - $geo => null, - 'coordinates' => '', - 'place' => '', - 'contributors' => '', - 'is_quote_status' => false, - 'retweet_count' => 0, - 'favorite_count' => 0, - 'favorited' => $lastwall['starred'] ? true : false, - 'retweeted' => false, - 'possibly_sensitive' => false, - 'lang' => '', - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $lastwall['parent'], - 'external_url' => System::baseUrl() . '/display/' . $lastwall['guid'], - ]; +/** + * Retrieves a single item record based on the provided condition and converts it for API use. + * + * @param array $condition Item table condition array + * @return array + * @throws Exception + */ +function api_get_item(array $condition) +{ + $item = Item::selectFirst(Item::DISPLAY_FIELDLIST, $condition, ['order' => ['id' => true]]); - if (count($converted["attachments"]) > 0) { - $status_info["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $status_info["entities"] = $converted["entities"]; - } - - if ($status_info["source"] == 'web') { - $status_info["source"] = ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']); - } elseif (ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']) != $status_info["source"]) { - $status_info["source"] = trim($status_info["source"].' ('.ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']).')'); - } - - // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($status_info["user"]["uid"]); - unset($status_info["user"]["self"]); - - Logger::log('status_info: '.print_r($status_info, true), Logger::DEBUG); - - if ($type == "raw") { - return $status_info; - } - - return api_format_data("statuses", $type, ['status' => $status_info]); - } + return $item; } /** @@ -1359,66 +1310,20 @@ function api_status_show($type, $item_id = 0) */ function api_users_show($type) { - $a = \get_app(); + $a = \Friendica\BaseObject::getApp(); $user_info = api_get_user($a); - $condition = ['owner-id' => $user_info['pid'], 'uid' => api_user(), - 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], 'private' => false]; - $lastwall = Item::selectFirst(Item::ITEM_FIELDLIST, $condition, ['order' => ['id' => true]]); - - if (DBA::isResult($lastwall)) { - $in_reply_to = api_in_reply_to($lastwall); - - $converted = api_convert_item($lastwall); - - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } - - $user_info['status'] = [ - 'text' => $converted["text"], - 'truncated' => false, - 'created_at' => api_date($lastwall['created']), - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'source' => (($lastwall['app']) ? $lastwall['app'] : 'web'), - 'id' => intval($lastwall['contact-id']), - 'id_str' => (string) $lastwall['contact-id'], - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - $geo => null, - 'favorited' => $lastwall['starred'] ? true : false, - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $lastwall['parent'], - 'external_url' => System::baseUrl() . "/display/" . $lastwall['guid'], - ]; - - if (count($converted["attachments"]) > 0) { - $user_info["status"]["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $user_info["status"]["entities"] = $converted["entities"]; - } - - if ($user_info["status"]["source"] == 'web') { - $user_info["status"]["source"] = ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']); - } - - if (ContactSelector::networkToName($lastwall['network'], $user_info['url']) != $user_info["status"]["source"]) { - $user_info["status"]["source"] = trim($user_info["status"]["source"] . ' (' . ContactSelector::networkToName($lastwall['network'], $lastwall['author-link']) . ')'); - } + $item = api_get_last_status($user_info['pid'], $user_info['uid']); + if ($item) { + $user_info['status'] = api_format_item($item, $type); } // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($user_info["uid"]); - unset($user_info["self"]); + unset($user_info['uid']); + unset($user_info['self']); - return api_format_data("user", $type, ['user' => $user_info]); + return api_format_data('user', $type, ['user' => $user_info]); } /// @TODO move to top of file or somewhere better @@ -2972,7 +2877,7 @@ function api_format_items_profiles($profile_row) /** * @brief format items to be returned by api * - * @param array $r array of items + * @param array $items array of items * @param array $user_info * @param bool $filter_user filter items by $user_info * @param string $type Return type (atom, rss, xml, json) @@ -2982,14 +2887,13 @@ function api_format_items_profiles($profile_row) * @throws InternalServerErrorException * @throws UnauthorizedException */ -function api_format_items($r, $user_info, $filter_user = false, $type = "json") +function api_format_items($items, $user_info, $filter_user = false, $type = "json") { - $a = \get_app(); + $a = \Friendica\BaseObject::getApp(); $ret = []; - foreach ((array)$r as $item) { - localize_item($item); + foreach ((array)$items as $item) { list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item); // Look if the posts are matching if they should be filtered by user id @@ -2997,100 +2901,128 @@ function api_format_items($r, $user_info, $filter_user = false, $type = "json") continue; } - $in_reply_to = api_in_reply_to($item); + $status = api_format_item($item, $type, $status_user, $author_user, $owner_user); - $converted = api_convert_item($item); - - if ($type == "xml") { - $geo = "georss:point"; - } else { - $geo = "geo"; - } - - $status = [ - 'text' => $converted["text"], - 'truncated' => false, - 'created_at'=> api_date($item['created']), - 'in_reply_to_status_id' => $in_reply_to['status_id'], - 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], - 'source' => (($item['app']) ? $item['app'] : 'web'), - 'id' => intval($item['id']), - 'id_str' => (string) intval($item['id']), - 'in_reply_to_user_id' => $in_reply_to['user_id'], - 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], - 'in_reply_to_screen_name' => $in_reply_to['screen_name'], - $geo => null, - 'favorited' => $item['starred'] ? true : false, - 'user' => $status_user, - 'friendica_author' => $author_user, - 'friendica_owner' => $owner_user, - 'friendica_private' => $item['private'] == 1, - //'entities' => NULL, - 'statusnet_html' => $converted["html"], - 'statusnet_conversation_id' => $item['parent'], - 'external_url' => System::baseUrl() . "/display/" . $item['guid'], - 'friendica_activities' => api_format_items_activities($item, $type), - ]; - - if (count($converted["attachments"]) > 0) { - $status["attachments"] = $converted["attachments"]; - } - - if (count($converted["entities"]) > 0) { - $status["entities"] = $converted["entities"]; - } - - if ($status["source"] == 'web') { - $status["source"] = ContactSelector::networkToName($item['network'], $item['author-link']); - } elseif (ContactSelector::networkToName($item['network'], $item['author-link']) != $status["source"]) { - $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['network'], $item['author-link']).')'); - } - - if ($item["id"] == $item["parent"]) { - $retweeted_item = api_share_as_retweet($item); - if ($retweeted_item !== false) { - $retweeted_status = $status; - $status['user'] = $status['friendica_owner']; - try { - $retweeted_status["user"] = api_get_user($a, $retweeted_item["author-id"]); - } catch (BadRequestException $e) { - // user not found. should be found? - /// @todo check if the user should be always found - $retweeted_status["user"] = []; - } - - $rt_converted = api_convert_item($retweeted_item); - - $retweeted_status['text'] = $rt_converted["text"]; - $retweeted_status['statusnet_html'] = $rt_converted["html"]; - $retweeted_status['friendica_activities'] = api_format_items_activities($retweeted_item, $type); - $retweeted_status['created_at'] = api_date($retweeted_item['created']); - $status['retweeted_status'] = $retweeted_status; - $status['friendica_author'] = $retweeted_status['friendica_author']; - } - } - - // "uid" and "self" are only needed for some internal stuff, so remove it from here - unset($status["user"]["uid"]); - unset($status["user"]["self"]); - - if ($item["coord"] != "") { - $coords = explode(' ', $item["coord"]); - if (count($coords) == 2) { - if ($type == "json") { - $status["geo"] = ['type' => 'Point', - 'coordinates' => [(float) $coords[0], - (float) $coords[1]]]; - } else {// Not sure if this is the official format - if someone founds a documentation we can check - $status["georss:point"] = $item["coord"]; - } - } - } $ret[] = $status; - }; + } + return $ret; } +/** + * @param array $item Item record + * @param string $type Return format (atom, rss, xml, json) + * @param array $status_user User record of the item author, can be provided by api_item_get_user() + * @param array $author_user User record of the item author, can be provided by api_item_get_user() + * @param array $owner_user User record of the item owner, can be provided by api_item_get_user() + * @return array API-formatted status + * @throws BadRequestException + * @throws ImagickException + * @throws InternalServerErrorException + * @throws UnauthorizedException + */ +function api_format_item($item, $type = "json", $status_user = null, $author_user = null, $owner_user = null) +{ + $a = \Friendica\BaseObject::getApp(); + + if (empty($status_user) || empty($author_user) || empty($owner_user)) { + list($status_user, $author_user, $owner_user) = api_item_get_user($a, $item); + } + + localize_item($item); + + $in_reply_to = api_in_reply_to($item); + + $converted = api_convert_item($item); + + if ($type == "xml") { + $geo = "georss:point"; + } else { + $geo = "geo"; + } + + $status = [ + 'text' => $converted["text"], + 'truncated' => false, + 'created_at'=> api_date($item['created']), + 'in_reply_to_status_id' => $in_reply_to['status_id'], + 'in_reply_to_status_id_str' => $in_reply_to['status_id_str'], + 'source' => (($item['app']) ? $item['app'] : 'web'), + 'id' => intval($item['id']), + 'id_str' => (string) intval($item['id']), + 'in_reply_to_user_id' => $in_reply_to['user_id'], + 'in_reply_to_user_id_str' => $in_reply_to['user_id_str'], + 'in_reply_to_screen_name' => $in_reply_to['screen_name'], + $geo => null, + 'favorited' => $item['starred'] ? true : false, + 'user' => $status_user, + 'friendica_author' => $author_user, + 'friendica_owner' => $owner_user, + 'friendica_private' => $item['private'] == 1, + //'entities' => NULL, + 'statusnet_html' => $converted["html"], + 'statusnet_conversation_id' => $item['parent'], + 'external_url' => System::baseUrl() . "/display/" . $item['guid'], + 'friendica_activities' => api_format_items_activities($item, $type), + ]; + + if (count($converted["attachments"]) > 0) { + $status["attachments"] = $converted["attachments"]; + } + + if (count($converted["entities"]) > 0) { + $status["entities"] = $converted["entities"]; + } + + if ($status["source"] == 'web') { + $status["source"] = ContactSelector::networkToName($item['network'], $item['author-link']); + } elseif (ContactSelector::networkToName($item['network'], $item['author-link']) != $status["source"]) { + $status["source"] = trim($status["source"].' ('.ContactSelector::networkToName($item['network'], $item['author-link']).')'); + } + + if ($item["id"] == $item["parent"]) { + $retweeted_item = api_share_as_retweet($item); + if ($retweeted_item !== false) { + $retweeted_status = $status; + $status['user'] = $status['friendica_owner']; + try { + $retweeted_status["user"] = api_get_user($a, $retweeted_item["author-id"]); + } catch (BadRequestException $e) { + // user not found. should be found? + /// @todo check if the user should be always found + $retweeted_status["user"] = []; + } + + $rt_converted = api_convert_item($retweeted_item); + + $retweeted_status['text'] = $rt_converted["text"]; + $retweeted_status['statusnet_html'] = $rt_converted["html"]; + $retweeted_status['friendica_activities'] = api_format_items_activities($retweeted_item, $type); + $retweeted_status['created_at'] = api_date($retweeted_item['created']); + $status['retweeted_status'] = $retweeted_status; + $status['friendica_author'] = $retweeted_status['friendica_author'];} + } + + // "uid" and "self" are only needed for some internal stuff, so remove it from here + unset($status["user"]["uid"]); + unset($status["user"]["self"]); + + if ($item["coord"] != "") { + $coords = explode(' ', $item["coord"]); + if (count($coords) == 2) { + if ($type == "json") { + $status["geo"] = ['type' => 'Point', + 'coordinates' => [(float) $coords[0], + (float) $coords[1]]]; + } else {// Not sure if this is the official format - if someone founds a documentation we can check + $status["georss:point"] = $item["coord"]; + } + } + } + + return $status; +} + /** * Returns the remaining number of API requests available to the user before the API limit is reached. * @@ -5957,7 +5889,7 @@ function api_friendica_notification($type) } $nm = new NotificationsManager(); - $notes = $nm->getAll([], "+seen -date", 50); + $notes = $nm->getAll([], ['seen' => 'ASC', 'date' => 'DESC'], 50); if ($type == "xml") { $xmlnotes = []; diff --git a/mod/admin.php b/mod/admin.php index f8a75b7a2d..7f8e12e0c7 100644 --- a/mod/admin.php +++ b/mod/admin.php @@ -923,6 +923,10 @@ function admin_page_summary(App $a) $showwarning = true; $warningtext[] = L10n::t('The database update failed. Please run "php bin/console.php dbstructure update" from the command line and have a look at the errors that might appear.'); } + if (Config::get('system', 'update') == Update::FAILED) { + $showwarning = true; + $warningtext[] = L10n::t('The last update failed. Please run "php bin/console.php dbstructure update" from the command line and have a look at the errors that might appear. (Some of the errors are possibly inside the logfile.)'); + } $last_worker_call = Config::get('system', 'last_worker_execution', false); if (!$last_worker_call) { @@ -1087,7 +1091,9 @@ function admin_page_site_post(App $a) update_table($a, "gcontact", ['connect', 'addr'], $old_host, $new_host); // update config - Config::set('system', 'hostname', parse_url($new_url, PHP_URL_HOST)); + $configFileSaver = new \Friendica\Util\Config\ConfigFileSaver($a->getBasePath()); + $configFileSaver->addConfigValue('config', 'hostname', parse_url($new_url, PHP_URL_HOST)); + $configFileSaver->saveToConfigFile(); Config::set('system', 'url', $new_url); $a->setBaseURL($new_url); @@ -1105,7 +1111,6 @@ function admin_page_site_post(App $a) // end relocate $sitename = (!empty($_POST['sitename']) ? Strings::escapeTags(trim($_POST['sitename'])) : ''); - $hostname = (!empty($_POST['hostname']) ? Strings::escapeTags(trim($_POST['hostname'])) : ''); $sender_email = (!empty($_POST['sender_email']) ? Strings::escapeTags(trim($_POST['sender_email'])) : ''); $banner = (!empty($_POST['banner']) ? trim($_POST['banner']) : false); $shortcut_icon = (!empty($_POST['shortcut_icon']) ? Strings::escapeTags(trim($_POST['shortcut_icon'])) : ''); @@ -1176,7 +1181,6 @@ function admin_page_site_post(App $a) $itemcache_duration = (!empty($_POST['itemcache_duration']) ? intval($_POST['itemcache_duration']) : 0); $max_comments = (!empty($_POST['max_comments']) ? intval($_POST['max_comments']) : 0); $temppath = (!empty($_POST['temppath']) ? Strings::escapeTags(trim($_POST['temppath'])) : ''); - $basepath = (!empty($_POST['basepath']) ? Strings::escapeTags(trim($_POST['basepath'])) : ''); $singleuser = (!empty($_POST['singleuser']) ? Strings::escapeTags(trim($_POST['singleuser'])) : ''); $proxy_disabled = !empty($_POST['proxy_disabled']); $only_tag_search = !empty($_POST['only_tag_search']); @@ -1296,7 +1300,6 @@ function admin_page_site_post(App $a) Config::set('system', 'poco_local_search' , $poco_local_search); Config::set('system', 'nodeinfo' , $nodeinfo); Config::set('config', 'sitename' , $sitename); - Config::set('config', 'hostname' , $hostname); Config::set('config', 'sender_email' , $sender_email); Config::set('system', 'suppress_tags' , $suppress_tags); Config::set('system', 'shortcut_icon' , $shortcut_icon); @@ -1392,11 +1395,6 @@ function admin_page_site_post(App $a) Config::set('system', 'temppath', $temppath); - if ($basepath != '') { - $basepath = BasePath::getRealPath($basepath); - } - - Config::set('system', 'basepath' , $basepath); Config::set('system', 'proxy_disabled' , $proxy_disabled); Config::set('system', 'only_tag_search' , $only_tag_search); @@ -1536,9 +1534,6 @@ function admin_page_site(App $a) "develop" => L10n::t("check the development version") ]; - if (empty(Config::get('config', 'hostname'))) { - Config::set('config', 'hostname', $a->getHostName()); - } $diaspora_able = ($a->getURLPath() == ""); $optimize_max_tablesize = Config::get('system', 'optimize_max_tablesize', -1); @@ -1597,7 +1592,6 @@ function admin_page_site(App $a) // name, label, value, help string, extra data... '$sitename' => ['sitename', L10n::t("Site name"), Config::get('config', 'sitename'), ''], - '$hostname' => ['hostname', L10n::t("Host name"), Config::get('config', 'hostname'), ""], '$sender_email' => ['sender_email', L10n::t("Sender Email"), Config::get('config', 'sender_email'), L10n::t("The email address your server shall use to send notification emails from."), "", "", "email"], '$banner' => ['banner', L10n::t("Banner/Logo"), $banner, ""], '$shortcut_icon' => ['shortcut_icon', L10n::t("Shortcut icon"), Config::get('system', 'shortcut_icon'), L10n::t("Link to an icon that will be used for browsers.")], @@ -1675,7 +1669,6 @@ function admin_page_site(App $a) '$itemcache_duration' => ['itemcache_duration', L10n::t("Cache duration in seconds"), Config::get('system', 'itemcache_duration'), L10n::t("How long should the cache files be hold? Default value is 86400 seconds \x28One day\x29. To disable the item cache, set the value to -1.")], '$max_comments' => ['max_comments', L10n::t("Maximum numbers of comments per post"), Config::get('system', 'max_comments'), L10n::t("How much comments should be shown for each post? Default value is 100.")], '$temppath' => ['temppath', L10n::t("Temp path"), Config::get('system', 'temppath'), L10n::t("If you have a restricted system where the webserver can't access the system temp path, enter another path here.")], - '$basepath' => ['basepath', L10n::t("Base path to installation"), Config::get('system', 'basepath'), L10n::t("If the system cannot detect the correct path to your installation, enter the correct path here. This setting should only be set if you are using a restricted system and symbolic links to your webroot.")], '$proxy_disabled' => ['proxy_disabled', L10n::t("Disable picture proxy"), Config::get('system', 'proxy_disabled'), L10n::t("The picture proxy increases performance and privacy. It shouldn't be used on systems with very low bandwidth.")], '$only_tag_search' => ['only_tag_search', L10n::t("Only search in tags"), Config::get('system', 'only_tag_search'), L10n::t("On large systems the text search can slow down the system extremely.")], diff --git a/mod/apps.php b/mod/apps.php deleted file mode 100644 index d497ce93a2..0000000000 --- a/mod/apps.php +++ /dev/null @@ -1,33 +0,0 @@ - $title, - '$apps' => $apps, - ]); -} diff --git a/mod/credits.php b/mod/credits.php deleted file mode 100644 index 3d71c54b3a..0000000000 --- a/mod/credits.php +++ /dev/null @@ -1,23 +0,0 @@ - L10n::t('Credits'), - '$thanks' => L10n::t('Friendica is a community project, that would not be possible without the help of many people. Here is a list of those who have contributed to the code or the translation of Friendica. Thank you all!'), - '$names' => $names, - ]); -} diff --git a/mod/feedtest.php b/mod/feedtest.php deleted file mode 100644 index cffc1f3451..0000000000 --- a/mod/feedtest.php +++ /dev/null @@ -1,50 +0,0 @@ - local_user()]); - - $contact_id = Contact::getIdForURL($url, local_user(), true); - - $contact = DBA::selectFirst('contact', [], ['id' => $contact_id]); - - $xml = Network::fetchUrl($contact['poll']); - - $dummy = null; - $import_result = Feed::import($xml, $importer, $contact, $dummy, true); - - $result = [ - 'input' => $xml, - 'output' => var_export($import_result, true), - ]; - } - - $tpl = Renderer::getMarkupTemplate('feedtest.tpl'); - $o = Renderer::replaceMacros($tpl, [ - '$url' => ['url', L10n::t('Source URL'), defaults($_REQUEST, 'url', ''), ''], - '$result' => $result - ]); - - return $o; -} diff --git a/mod/filer.php b/mod/filer.php deleted file mode 100644 index 0d9afe638b..0000000000 --- a/mod/filer.php +++ /dev/null @@ -1,42 +0,0 @@ -argc > 1) ? intval($a->argv[1]) : 0); - - Logger::log('filer: tag ' . $term . ' item ' . $item_id); - - if ($item_id && strlen($term)) { - // file item - FileTag::saveFile(local_user(), $item_id, $term); - } else { - // return filer dialog - $filetags = PConfig::get(local_user(), 'system', 'filetags'); - $filetags = FileTag::fileToList($filetags, 'file'); - $filetags = explode(",", $filetags); - - $tpl = Renderer::getMarkupTemplate("filer_dialog.tpl"); - $o = Renderer::replaceMacros($tpl, [ - '$field' => ['term', L10n::t("Save to Folder:"), '', '', $filetags, L10n::t('- select -')], - '$submit' => L10n::t('Save'), - ]); - - echo $o; - } - exit(); -} diff --git a/src/App.php b/src/App.php index b5ad683219..c0afd514d0 100644 --- a/src/App.php +++ b/src/App.php @@ -8,12 +8,12 @@ use Detection\MobileDetect; use DOMDocument; use DOMXPath; use Exception; -use Friendica\Core\Config\Cache\ConfigCacheLoader; use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Core\Config\Configuration; use Friendica\Database\DBA; use Friendica\Model\Profile; use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\Config\ConfigFileLoader; use Friendica\Util\HTTPSignature; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; @@ -75,11 +75,6 @@ class App */ private $mode; - /** - * @var string The App base path - */ - private $basePath; - /** * @var string The App URL path */ @@ -142,7 +137,7 @@ class App */ public function getBasePath() { - return $this->basePath; + return $this->config->get('system', 'basepath'); } /** @@ -165,6 +160,16 @@ class App return $this->profiler; } + /** + * Returns the Mode of the Application + * + * @return App\Mode The Application Mode + */ + public function getMode() + { + return $this->mode; + } + /** * Register a stylesheet file path to be included in the tag of every page. * Inclusion is done in App->initHead(). @@ -177,7 +182,7 @@ class App */ public function registerStylesheet($path) { - $url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path); + $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path); $this->stylesheets[] = trim($url, '/'); } @@ -194,7 +199,7 @@ class App */ public function registerFooterScript($path) { - $url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path); + $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path); $this->footerScripts[] = trim($url, '/'); } @@ -206,36 +211,28 @@ class App /** * @brief App constructor. * - * @param string $basePath The basedir of the app * @param Configuration $config The Configuration + * @param App\Mode $mode The mode of this Friendica app * @param LoggerInterface $logger The current app logger * @param Profiler $profiler The profiler of this application * @param bool $isBackend Whether it is used for backend or frontend (Default true=backend) * * @throws Exception if the Basepath is not usable */ - public function __construct($basePath, Configuration $config, LoggerInterface $logger, Profiler $profiler, $isBackend = true) + public function __construct(Configuration $config, App\Mode $mode, LoggerInterface $logger, Profiler $profiler, $isBackend = true) { BaseObject::setApp($this); $this->logger = $logger; $this->config = $config; $this->profiler = $profiler; - $cfgBasePath = $this->config->get('system', 'basepath'); - $this->basePath = !empty($cfgBasePath) ? $cfgBasePath : $basePath; - - if (!Core\System::isDirectoryUsable($this->basePath, false)) { - throw new Exception('Basepath \'' . $this->basePath . '\' isn\'t usable.'); - } - $this->basePath = rtrim($this->basePath, DIRECTORY_SEPARATOR); + $this->mode = $mode; $this->checkBackend($isBackend); $this->checkFriendicaApp(); $this->profiler->reset(); - $this->mode = new App\Mode($this->basePath); - $this->reload(); set_time_limit(0); @@ -265,9 +262,9 @@ class App set_include_path( get_include_path() . PATH_SEPARATOR - . $this->basePath . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR - . $this->basePath . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR - . $this->basePath); + . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR + . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR + . $this->getBasePath()); if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) { $this->query_string = substr($_SERVER['QUERY_STRING'], 9); @@ -335,22 +332,6 @@ class App Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine'); } - /** - * Returns the Mode of the Application - * - * @return App\Mode The Application Mode - * - * @throws InternalServerErrorException when the mode isn't created - */ - public function getMode() - { - if (empty($this->mode)) { - throw new InternalServerErrorException('Mode of the Application is not defined'); - } - - return $this->mode; - } - /** * Reloads the whole app instance */ @@ -358,10 +339,10 @@ class App { $this->determineURLPath(); - $this->getMode()->determine($this->basePath); + $this->getMode()->determine($this->getBasePath()); if ($this->getMode()->has(App\Mode::DBAVAILABLE)) { - $loader = new ConfigCacheLoader($this->basePath); + $loader = new ConfigFileLoader($this->getBasePath(), $this->getMode()); $this->config->getCache()->load($loader->loadCoreConfig('addon'), true); $this->profiler->update( @@ -369,6 +350,7 @@ class App $this->config->get('rendertime', 'callstack', false)); Core\Hook::loadHooks(); + $loader = new ConfigFileLoader($this->getBasePath(), $this->mode); Core\Hook::callAll('load_config', $loader); } @@ -470,14 +452,14 @@ class App { $scheme = $this->scheme; - if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) { + if ($this->config->get('system', 'ssl_policy') == SSL_POLICY_FULL) { $scheme = 'https'; } // Basically, we have $ssl = true on any links which can only be seen by a logged in user // (and also the login link). Anything seen by an outsider will have it turned off. - if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) { + if ($this->config->get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) { if ($ssl) { $scheme = 'https'; } else { @@ -485,8 +467,8 @@ class App } } - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); + if ($this->config->get('config', 'hostname') != '') { + $this->hostname = $this->config->get('config', 'hostname'); } return $scheme . '://' . $this->hostname . (!empty($this->getURLPath()) ? '/' . $this->getURLPath() : '' ); @@ -521,12 +503,12 @@ class App $this->urlPath = trim($parsed['path'], '\\/'); } - if (file_exists($this->basePath . '/.htpreconfig.php')) { - include $this->basePath . '/.htpreconfig.php'; + if (file_exists($this->getBasePath() . '/.htpreconfig.php')) { + include $this->getBasePath() . '/.htpreconfig.php'; } - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); + if ($this->config->get('config', 'hostname') != '') { + $this->hostname = $this->config->get('config', 'hostname'); } if (!isset($this->hostname) || ($this->hostname == '')) { @@ -537,8 +519,8 @@ class App public function getHostName() { - if (Core\Config::get('config', 'hostname') != '') { - $this->hostname = Core\Config::get('config', 'hostname'); + if ($this->config->get('config', 'hostname') != '') { + $this->hostname = $this->config->get('config', 'hostname'); } return $this->hostname; @@ -588,12 +570,12 @@ class App $this->registerStylesheet($stylesheet); - $shortcut_icon = Core\Config::get('system', 'shortcut_icon'); + $shortcut_icon = $this->config->get('system', 'shortcut_icon'); if ($shortcut_icon == '') { $shortcut_icon = 'images/friendica-32.png'; } - $touch_icon = Core\Config::get('system', 'touch_icon'); + $touch_icon = $this->config->get('system', 'touch_icon'); if ($touch_icon == '') { $touch_icon = 'images/friendica-128.png'; } @@ -613,7 +595,7 @@ class App '$update_interval' => $interval, '$shortcut_icon' => $shortcut_icon, '$touch_icon' => $touch_icon, - '$block_public' => intval(Core\Config::get('system', 'block_public')), + '$block_public' => intval($this->config->get('system', 'block_public')), '$stylesheets' => $this->stylesheets, ]) . $this->page['htmlhead']; } @@ -742,6 +724,7 @@ class App 'fetch', 'hcard', 'hostxrd', + 'manifest', 'nodeinfo', 'noscrape', 'p', @@ -786,13 +769,13 @@ class App * if ($this->is_backend()) { $process = 'backend'; - $max_processes = Core\Config::get('system', 'max_processes_backend'); + $max_processes = $this->config->get('system', 'max_processes_backend'); if (intval($max_processes) == 0) { $max_processes = 5; } } else { $process = 'frontend'; - $max_processes = Core\Config::get('system', 'max_processes_frontend'); + $max_processes = $this->config->get('system', 'max_processes_frontend'); if (intval($max_processes) == 0) { $max_processes = 20; } @@ -819,7 +802,7 @@ class App */ public function isMinMemoryReached() { - $min_memory = Core\Config::get('system', 'min_memory', 0); + $min_memory = $this->config->get('system', 'min_memory', 0); if ($min_memory == 0) { return false; } @@ -866,13 +849,13 @@ class App { if ($this->isBackend()) { $process = 'backend'; - $maxsysload = intval(Core\Config::get('system', 'maxloadavg')); + $maxsysload = intval($this->config->get('system', 'maxloadavg')); if ($maxsysload < 1) { $maxsysload = 50; } } else { $process = 'frontend'; - $maxsysload = intval(Core\Config::get('system', 'maxloadavg_frontend')); + $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend')); if ($maxsysload < 1) { $maxsysload = 50; } @@ -919,9 +902,9 @@ class App } if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { - $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->basePath); + $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath()); } else { - $resource = proc_open($cmdline . ' &', [], $foo, $this->basePath); + $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath()); } if (!is_resource($resource)) { Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG); @@ -938,7 +921,7 @@ class App */ public function getSenderEmailAddress() { - $sender_email = Core\Config::get('config', 'sender_email'); + $sender_email = $this->config->get('config', 'sender_email'); if (empty($sender_email)) { $hostname = $this->getHostName(); if (strpos($hostname, ':')) { @@ -982,7 +965,7 @@ class App */ private function computeCurrentTheme() { - $system_theme = Core\Config::get('system', 'theme'); + $system_theme = $this->config->get('system', 'theme'); if (!$system_theme) { throw new Exception(Core\L10n::t('No system theme config value set.')); } @@ -990,7 +973,7 @@ class App // Sane default $this->currentTheme = $system_theme; - $allowed_themes = explode(',', Core\Config::get('system', 'allowed_themes', $system_theme)); + $allowed_themes = explode(',', $this->config->get('system', 'allowed_themes', $system_theme)); $page_theme = null; // Find the theme that belongs to the user whose stuff we are looking at @@ -1007,7 +990,7 @@ class App // Specific mobile theme override if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) { - $system_mobile_theme = Core\Config::get('system', 'mobile-theme'); + $system_mobile_theme = $this->config->get('system', 'mobile-theme'); $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme); // --- means same mobile theme as desktop @@ -1078,7 +1061,7 @@ class App */ public function checkURL() { - $url = Core\Config::get('system', 'url'); + $url = $this->config->get('system', 'url'); // if the url isn't set or the stored url is radically different // than the currently visited url, store the current value accordingly. @@ -1087,7 +1070,7 @@ class App // We will only change the url to an ip address if there is no existing setting if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->getHostName()))) { - Core\Config::set('system', 'url', $this->getBaseURL()); + $this->config->set('system', 'url', $this->getBaseURL()); } } @@ -1120,8 +1103,8 @@ class App if (!$this->getMode()->isInstall()) { // Force SSL redirection - if (Core\Config::get('system', 'force_ssl') && ($this->getScheme() == "http") - && intval(Core\Config::get('system', 'ssl_policy')) == SSL_POLICY_FULL + if ($this->config->get('system', 'force_ssl') && ($this->getScheme() == "http") + && intval($this->config->get('system', 'ssl_policy')) == SSL_POLICY_FULL && strpos($this->getBaseURL(), 'https://') === 0 && $_SERVER['REQUEST_METHOD'] == 'GET') { header('HTTP/1.1 302 Moved Temporarily'); @@ -1204,7 +1187,7 @@ class App $this->module = 'maintenance'; } else { $this->checkURL(); - Core\Update::check($this->basePath, false); + Core\Update::check($this->getBasePath(), false); Core\Addon::loadAddons(); Core\Hook::loadHooks(); } @@ -1261,7 +1244,7 @@ class App $this->module = "login"; } - $privateapps = Core\Config::get('config', 'private_addons', false); + $privateapps = $this->config->get('config', 'private_addons', false); if (Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) { //Check if module is an app and if public access to apps is allowed or not if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) { @@ -1446,7 +1429,7 @@ class App header("X-Friendica-Version: " . FRIENDICA_VERSION); header("Content-type: text/html; charset=utf-8"); - if (Core\Config::get('system', 'hsts') && (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL)) { + if ($this->config->get('system', 'hsts') && ($this->config->get('system', 'ssl_policy') == SSL_POLICY_FULL)) { header("Strict-Transport-Security: max-age=31536000"); } diff --git a/src/Content/ForumManager.php b/src/Content/ForumManager.php index 0ef18df35f..e9dab41ef9 100644 --- a/src/Content/ForumManager.php +++ b/src/Content/ForumManager.php @@ -196,7 +196,7 @@ class ForumManager */ public static function countUnseenItems() { - $r = q( + $stmtContacts = DBA::p( "SELECT `contact`.`id`, `contact`.`name`, COUNT(*) AS `count` FROM `item` INNER JOIN `contact` ON `item`.`contact-id` = `contact`.`id` WHERE `item`.`uid` = %d AND `item`.`visible` AND NOT `item`.`deleted` AND `item`.`unseen` @@ -208,6 +208,6 @@ class ForumManager intval(local_user()) ); - return $r; + return DBA::toArray($stmtContacts); } } diff --git a/src/Content/Smilies.php b/src/Content/Smilies.php index 9023959978..9fbfd2d629 100644 --- a/src/Content/Smilies.php +++ b/src/Content/Smilies.php @@ -213,7 +213,6 @@ class Smilies return $text; } - $text = preg_replace_callback('/
(.*?)<\/pre>/ism'  , 'self::encode', $text);
 		$text = preg_replace_callback('/(.*?)<\/code>/ism', 'self::encode', $text);
 
 		if ($no_images) {
@@ -231,7 +230,6 @@ class Smilies
 		$text = preg_replace_callback('/<(3+)/', 'self::pregHeart', $text);
 		$text = self::strOrigReplace($smilies['texts'], $smilies['icons'], $text);
 
-		$text = preg_replace_callback('/
(.*?)<\/pre>/ism', 'self::decode', $text);
 		$text = preg_replace_callback('/(.*?)<\/code>/ism', 'self::decode', $text);
 
 		return $text;
@@ -244,7 +242,7 @@ class Smilies
 	 */
 	private static function encode($m)
 	{
-		return(str_replace($m[1], Strings::base64UrlEncode($m[1]), $m[0]));
+		return '' . Strings::base64UrlEncode($m[1]) . '';
 	}
 
 	/**
@@ -255,7 +253,7 @@ class Smilies
 	 */
 	private static function decode($m)
 	{
-		return(str_replace($m[1], Strings::base64UrlDecode($m[1]), $m[0]));
+		return '' . Strings::base64UrlDecode($m[1]) . '';
 	}
 
 
diff --git a/src/Core/ACL.php b/src/Core/ACL.php
index 19015714e9..e6c82fd4bf 100644
--- a/src/Core/ACL.php
+++ b/src/Core/ACL.php
@@ -266,14 +266,12 @@ class ACL extends BaseObject
 			$default_permissions = self::getDefaultUserPermissions($user);
 		}
 
-		$jotnets = '';
+		$jotnets_fields = [];
 		if ($show_jotnets) {
-			$imap_disabled = !function_exists('imap_open') || Config::get('system', 'imap_disabled');
-
 			$mail_enabled = false;
 			$pubmail_enabled = false;
 
-			if (!$imap_disabled) {
+			if (function_exists('imap_open') && !Config::get('system', 'imap_disabled')) {
 				$mailacct = DBA::selectFirst('mailacct', ['pubmail'], ['`uid` = ? AND `server` != ""', local_user()]);
 				if (DBA::isResult($mailacct)) {
 					$mail_enabled = true;
@@ -283,17 +281,20 @@ class ACL extends BaseObject
 
 			if (empty($default_permissions['hidewall'])) {
 				if ($mail_enabled) {
-					$selected = $pubmail_enabled ? ' checked="checked"' : '';
-					$jotnets .= '
' . L10n::t("Post to Email") . '
'; + $jotnets_fields[] = [ + 'type' => 'checkbox', + 'field' => [ + 'pubmail_enable', + L10n::t('Post to Email'), + $pubmail_enabled + ] + ]; } - Hook::callAll('jot_networks', $jotnets); - } else { - $jotnets .= L10n::t('Connectors disabled, since "%s" is enabled.', - L10n::t('Hide your profile details from unknown viewers?')); + Hook::callAll('jot_networks', $jotnets_fields); } } - + $tpl = Renderer::getMarkupTemplate('acl_selector.tpl'); $o = Renderer::replaceMacros($tpl, [ '$showall' => L10n::t('Visible to everybody'), @@ -306,7 +307,10 @@ class ACL extends BaseObject '$networks' => $show_jotnets, '$emailcc' => L10n::t('CC: email addresses'), '$emtitle' => L10n::t('Example: bob@example.com, mary@example.com'), - '$jotnets' => $jotnets, + '$jotnets_enabled' => empty($default_permissions['hidewall']), + '$jotnets_summary' => L10n::t('Connectors'), + '$jotnets_fields' => $jotnets_fields, + '$jotnets_disabled_label' => L10n::t('Connectors disabled, since "%s" is enabled.', L10n::t('Hide your profile details from unknown viewers?')), '$aclModalTitle' => L10n::t('Permissions'), '$aclModalDismiss' => L10n::t('Close'), '$features' => [ diff --git a/src/Core/Cache.php b/src/Core/Cache.php index cadb2444b5..7a8f7367ec 100644 --- a/src/Core/Cache.php +++ b/src/Core/Cache.php @@ -4,7 +4,7 @@ */ namespace Friendica\Core; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; /** * @brief Class for storing data for a short time diff --git a/src/Core/Config/Cache/ConfigCache.php b/src/Core/Config/Cache/ConfigCache.php index cb299eb330..f61865cee6 100644 --- a/src/Core/Config/Cache/ConfigCache.php +++ b/src/Core/Config/Cache/ConfigCache.php @@ -5,7 +5,7 @@ namespace Friendica\Core\Config\Cache; /** * The Friendica config cache for the application * Initial, all *.config.php files are loaded into this cache with the - * ConfigCacheLoader ( @see ConfigCacheLoader ) + * ConfigFileLoader ( @see ConfigFileLoader ) */ class ConfigCache implements IConfigCache, IPConfigCache { diff --git a/src/Core/Config/Configuration.php b/src/Core/Config/Configuration.php index 532ed982a9..c6fe626d91 100644 --- a/src/Core/Config/Configuration.php +++ b/src/Core/Config/Configuration.php @@ -10,6 +10,16 @@ namespace Friendica\Core\Config; */ class Configuration { + /** + * The blacklist of configuration settings, which should not get saved to the backend + * @var array + */ + private $configSaveBlacklist = [ + 'config' => [ + 'hostname' => true, + ] + ]; + /** * @var Cache\IConfigCache */ @@ -117,7 +127,7 @@ class Configuration $cached = $this->configCache->set($cat, $key, $value); // If there is no connected adapter, we're finished - if (!$this->configAdapter->isConnected()) { + if (!$this->configAdapter->isConnected() || !empty($this->configSaveBlacklist[$cat][$key])) { return $cached; } diff --git a/src/Core/Console/AutomaticInstallation.php b/src/Core/Console/AutomaticInstallation.php index 9d1e8506cc..911c1c00a8 100644 --- a/src/Core/Console/AutomaticInstallation.php +++ b/src/Core/Console/AutomaticInstallation.php @@ -7,6 +7,7 @@ use Friendica\BaseObject; use Friendica\Core\Config; use Friendica\Core\Installer; use Friendica\Core\Theme; +use Friendica\Util\Config\ConfigFileLoader; use RuntimeException; class AutomaticInstallation extends Console @@ -74,6 +75,8 @@ HELP; $installer = new Installer(); + $configCache = $a->getConfigCache(); + $this->out(" Complete!\n\n"); // Check Environment @@ -81,7 +84,7 @@ HELP; $installer->resetChecks(); - if (!$this->runBasicChecks($installer)) { + if (!$this->runBasicChecks($installer, $configCache)) { $errorMessage = $this->extractErrors($installer->getChecks()); throw new RuntimeException($errorMessage); } @@ -100,41 +103,51 @@ HELP; } } - $db_host = $a->getConfigCache()->get('database', 'hostname'); - $db_user = $a->getConfigCache()->get('database', 'username'); - $db_pass = $a->getConfigCache()->get('database', 'password'); - $db_data = $a->getConfigCache()->get('database', 'database'); + //reload the config cache + $loader = new ConfigFileLoader($a->getBasePath(), $a->getMode()); + $loader->setupCache($configCache); + } else { // Creating config file $this->out("Creating config file...\n"); $save_db = $this->getOption(['s', 'savedb'], false); - $db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? getenv('MYSQL_HOST') : ''); + //$db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? (getenv('MYSQL_HOST') ? getenv('MYSQL_HOST') : Installer::DEFAULT_HOST) : ''); + $db_host = $this->getOption(['H', 'dbhost'], ($save_db) ? (getenv('MYSQL_HOST')) : Installer::DEFAULT_HOST); $db_port = $this->getOption(['p', 'dbport'], ($save_db) ? getenv('MYSQL_PORT') : null); - $db_data = $this->getOption(['d', 'dbdata'], ($save_db) ? getenv('MYSQL_DATABASE') : ''); - $db_user = $this->getOption(['U', 'dbuser'], ($save_db) ? getenv('MYSQL_USER') . getenv('MYSQL_USERNAME') : ''); - $db_pass = $this->getOption(['P', 'dbpass'], ($save_db) ? getenv('MYSQL_PASSWORD') : ''); - $url_path = $this->getOption(['u', 'urlpath'], !empty('FRIENDICA_URL_PATH') ? getenv('FRIENDICA_URL_PATH') : null); + $configCache->set('database', 'hostname', $db_host . (!empty($db_port) ? ':' . $db_port : '')); + $configCache->set('database', 'database', + $this->getOption(['d', 'dbdata'], + ($save_db) ? getenv('MYSQL_DATABASE') : '')); + $configCache->set('database', 'username', + $this->getOption(['U', 'dbuser'], + ($save_db) ? getenv('MYSQL_USER') . getenv('MYSQL_USERNAME') : '')); + $configCache->set('database', 'password', + $this->getOption(['P', 'dbpass'], + ($save_db) ? getenv('MYSQL_PASSWORD') : '')); $php_path = $this->getOption(['b', 'phppath'], !empty('FRIENDICA_PHP_PATH') ? getenv('FRIENDICA_PHP_PATH') : null); - $admin_mail = $this->getOption(['A', 'admin'], !empty('FRIENDICA_ADMIN_MAIL') ? getenv('FRIENDICA_ADMIN_MAIL') : ''); - $tz = $this->getOption(['T', 'tz'], !empty('FRIENDICA_TZ') ? getenv('FRIENDICA_TZ') : ''); - $lang = $this->getOption(['L', 'lang'], !empty('FRIENDICA_LANG') ? getenv('FRIENDICA_LANG') : ''); + if (!empty($php_path)) { + $configCache->set('config', 'php_path', $php_path); + } + $configCache->set('config', 'admin_email', + $this->getOption(['A', 'admin'], + !empty(getenv('FRIENDICA_ADMIN_MAIL')) ? getenv('FRIENDICA_ADMIN_MAIL') : '')); + $configCache->set('system', 'default_timezone', + $this->getOption(['T', 'tz'], + !empty(getenv('FRIENDICA_TZ')) ? getenv('FRIENDICA_TZ') : Installer::DEFAULT_TZ)); + $configCache->set('system', 'language', + $this->getOption(['L', 'lang'], + !empty(getenv('FRIENDICA_LANG')) ? getenv('FRIENDICA_LANG') : Installer::DEFAULT_LANG)); + if (empty($php_path)) { - $php_path = $installer->getPHPPath(); + $configCache->set('config', 'php_path', $installer->getPHPPath()); } $installer->createConfig( - $php_path, - $url_path, - (!empty($db_port) ? $db_host . ':' . $db_port : $db_host), - $db_user, - $db_pass, - $db_data, - $tz, - $lang, - $admin_mail, + $a, + $configCache, $a->getBasePath() ); } @@ -146,7 +159,7 @@ HELP; $installer->resetChecks(); - if (!$installer->checkDB($a->getBasePath(), $a->getConfigCache(), $a->getProfiler(), $db_host, $db_user, $db_pass, $db_data)) { + if (!$installer->checkDB($a->getBasePath(), $configCache, $a->getProfiler())) { $errorMessage = $this->extractErrors($installer->getChecks()); throw new RuntimeException($errorMessage); } @@ -180,12 +193,13 @@ HELP; } /** - * @param Installer $installer the Installer instance + * @param Installer $installer The Installer instance + * @param Config\Cache\IConfigCache $configCache The config cache * * @return bool true if checks were successfully, otherwise false * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function runBasicChecks(Installer $installer) + private function runBasicChecks(Installer $installer, Config\Cache\IConfigCache $configCache) { $checked = true; @@ -207,8 +221,8 @@ HELP; } $php_path = null; - if (!empty(Config::get('config', 'php_path'))) { - $php_path = Config::get('config', 'php_path'); + if ($configCache->has('config', 'php_path')) { + $php_path = $configCache->get('config', 'php_path'); } if (!$installer->checkPHP($php_path, true)) { diff --git a/src/Core/Installer.php b/src/Core/Installer.php index b6090bddb6..65561b097e 100644 --- a/src/Core/Installer.php +++ b/src/Core/Installer.php @@ -6,6 +6,7 @@ namespace Friendica\Core; use DOMDocument; use Exception; +use Friendica\App; use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Database\DBA; use Friendica\Database\DBStructure; @@ -129,33 +130,28 @@ class Installer * - Creates `config/local.config.php` * - Installs Database Structure * - * @param string $phppath Path to the PHP-Binary (optional, if not set e.g. 'php' or '/usr/bin/php') - * @param string $urlpath Path based on the URL of Friendica (e.g. '/friendica') - * @param string $dbhost Hostname/IP of the Friendica Database - * @param string $dbuser Username of the Database connection credentials - * @param string $dbpass Password of the Database connection credentials - * @param string $dbdata Name of the Database - * @param string $timezone Timezone of the Friendica Installaton (e.g. 'Europe/Berlin') - * @param string $language 2-letter ISO 639-1 code (eg. 'en') - * @param string $adminmail Mail-Adress of the administrator + * @param App $app The Friendica App + * @param IConfigCache $configCache The config cache with all config relevant information * @param string $basepath The basepath of Friendica * * @return bool true if the config was created, otherwise false * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function createConfig($phppath, $urlpath, $dbhost, $dbuser, $dbpass, $dbdata, $timezone, $language, $adminmail, $basepath) + public function createConfig(App $app, IConfigCache $configCache, $basepath) { $tpl = Renderer::getMarkupTemplate('local.config.tpl'); $txt = Renderer::replaceMacros($tpl, [ - '$phpath' => $phppath, - '$dbhost' => $dbhost, - '$dbuser' => $dbuser, - '$dbpass' => $dbpass, - '$dbdata' => $dbdata, - '$timezone' => $timezone, - '$language' => $language, - '$urlpath' => $urlpath, - '$adminmail' => $adminmail, + '$dbhost' => $configCache->get('database', 'hostname'), + '$dbuser' => $configCache->get('database', 'username'), + '$dbpass' => $configCache->get('database', 'password'), + '$dbdata' => $configCache->get('database', 'database'), + + '$phpath' => $this->getPHPPath(), + '$adminmail' => $configCache->get('config', 'admin_email'), + + '$timezone' => $configCache->get('system', 'default_timezone'), + '$language' => $configCache->get('system', 'language'), + '$urlpath' => $app->getURLPath(), ]); $result = file_put_contents($basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.config.php', $txt); @@ -594,16 +590,17 @@ class Installer * @param string $basePath The basepath of this call * @param IConfigCache $configCache The configuration cache * @param Profiler $profiler The profiler of this app - * @param string $dbhost Hostname/IP of the Friendica Database - * @param string $dbuser Username of the Database connection credentials - * @param string $dbpass Password of the Database connection credentials - * @param string $dbdata Name of the Database * * @return bool true if the check was successful, otherwise false * @throws Exception */ - public function checkDB($basePath, IConfigCache $configCache, Profiler $profiler, $dbhost, $dbuser, $dbpass, $dbdata) + public function checkDB($basePath, IConfigCache $configCache, Profiler $profiler) { + $dbhost = $configCache->get('database', 'hostname'); + $dbuser = $configCache->get('database', 'username'); + $dbpass = $configCache->get('database', 'password'); + $dbdata = $configCache->get('database', 'database'); + if (!DBA::connect($basePath, $configCache, $profiler, $dbhost, $dbuser, $dbpass, $dbdata)) { $this->addCheck(L10n::t('Could not connect to database.'), false, true, ''); diff --git a/src/Core/Lock.php b/src/Core/Lock.php index 8bc2c242d1..a45490bf39 100644 --- a/src/Core/Lock.php +++ b/src/Core/Lock.php @@ -7,7 +7,7 @@ namespace Friendica\Core; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Core\Cache\IMemoryCacheDriver; /** diff --git a/src/Core/Logger.php b/src/Core/Logger.php index 3cb22e1e47..fc2dde1dfe 100644 --- a/src/Core/Logger.php +++ b/src/Core/Logger.php @@ -4,14 +4,13 @@ */ namespace Friendica\Core; -use Friendica\BaseObject; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; /** * @brief Logger functions */ -class Logger extends BaseObject +class Logger { /** * @see Logger::error() @@ -96,13 +95,7 @@ class Logger extends BaseObject */ public static function emergency($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->emergency($message, $context); - self::getApp()->GetProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -120,13 +113,7 @@ class Logger extends BaseObject */ public static function alert($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->alert($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -143,13 +130,7 @@ class Logger extends BaseObject */ public static function critical($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->critical($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -165,14 +146,7 @@ class Logger extends BaseObject */ public static function error($message, $context = []) { - if (!isset(self::$logger)) { - echo "not set!?\n"; - return; - } - - $stamp1 = microtime(true); self::$logger->error($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -190,13 +164,7 @@ class Logger extends BaseObject */ public static function warning($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->warning($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -211,13 +179,7 @@ class Logger extends BaseObject */ public static function notice($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->notice($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -234,13 +196,7 @@ class Logger extends BaseObject */ public static function info($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->info($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -255,13 +211,7 @@ class Logger extends BaseObject */ public static function debug($message, $context = []) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->debug($message, $context); - self::getApp()->getProfiler()->saveTimestamp($stamp1, 'file', System::callstack()); } /** @@ -275,13 +225,7 @@ class Logger extends BaseObject */ public static function log($msg, $level = LogLevel::INFO) { - if (!isset(self::$logger)) { - return; - } - - $stamp1 = microtime(true); self::$logger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } /** @@ -296,12 +240,10 @@ class Logger extends BaseObject */ public static function devLog($msg, $level = LogLevel::DEBUG) { - if (!isset(self::$logger)) { + if (!isset(self::$devLogger)) { return; } - $stamp1 = microtime(true); self::$devLogger->log($level, $msg); - self::getApp()->getProfiler()->saveTimestamp($stamp1, "file", System::callstack()); } } diff --git a/src/Core/NotificationsManager.php b/src/Core/NotificationsManager.php index d582f21596..bc1d1a00aa 100644 --- a/src/Core/NotificationsManager.php +++ b/src/Core/NotificationsManager.php @@ -36,7 +36,7 @@ class NotificationsManager extends BaseObject * - msg_plain: message as plain text string * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private function _set_extra($notes) + private function _set_extra(array $notes) { $rets = []; foreach ($notes as $n) { @@ -55,50 +55,28 @@ class NotificationsManager extends BaseObject * @brief Get all notifications for local_user() * * @param array $filter optional Array "column name"=>value: filter query by columns values - * @param string $order optional Space separated list of column to sort by. - * Prepend name with "+" to sort ASC, "-" to sort DESC. Default to "-date" + * @param array $order optional Array to order by * @param string $limit optional Query limits * - * @return array of results or false on errors + * @return array|bool of results or false on errors * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public function getAll($filter = [], $order = "-date", $limit = "") + public function getAll($filter = [], $order = ['date' => 'DESC'], $limit = "") { - $filter_str = []; - $filter_sql = ""; - foreach ($filter as $column => $value) { - $filter_str[] = sprintf("`%s` = '%s'", $column, DBA::escape($value)); - } - if (count($filter_str) > 0) { - $filter_sql = "AND " . implode(" AND ", $filter_str); + $params = []; + + $params['order'] = $order; + + if (!empty($limit)) { + $params['limit'] = $limit; } - $aOrder = explode(" ", $order); - $asOrder = []; - foreach ($aOrder as $o) { - $dir = "asc"; - if ($o[0] === "-") { - $dir = "desc"; - $o = substr($o, 1); - } - if ($o[0] === "+") { - $dir = "asc"; - $o = substr($o, 1); - } - $asOrder[] = "$o $dir"; - } - $order_sql = implode(", ", $asOrder); + $dbFilter = array_merge($filter, ['uid' => local_user()]); - if ($limit != "") { - $limit = " LIMIT " . $limit; - } - $r = q( - "SELECT * FROM `notify` WHERE `uid` = %d $filter_sql ORDER BY $order_sql $limit", - intval(local_user()) - ); + $stmtNotifies = DBA::select('notify', [], $dbFilter, $params); - if (DBA::isResult($r)) { - return $this->_set_extra($r); + if (DBA::isResult($stmtNotifies)) { + return $this->_set_extra(DBA::toArray($stmtNotifies)); } return false; @@ -113,13 +91,9 @@ class NotificationsManager extends BaseObject */ public function getByID($id) { - $r = q( - "SELECT * FROM `notify` WHERE `id` = %d AND `uid` = %d LIMIT 1", - intval($id), - intval(local_user()) - ); - if (DBA::isResult($r)) { - return $this->_set_extra($r)[0]; + $stmtNotify = DBA::selectFirst('notify', [], ['id' => $id, 'uid' => local_user()]); + if (DBA::isResult($stmtNotify)) { + return $this->_set_extra([$stmtNotify])[0]; } return null; } @@ -134,14 +108,13 @@ class NotificationsManager extends BaseObject */ public function setSeen($note, $seen = true) { - return q( - "UPDATE `notify` SET `seen` = %d WHERE (`link` = '%s' OR (`parent` != 0 AND `parent` = %d AND `otype` = '%s')) AND `uid` = %d", - intval($seen), - DBA::escape($note['link']), - intval($note['parent']), - DBA::escape($note['otype']), - intval(local_user()) - ); + return DBA::update('notify', ['seen' => $seen], [ + '(`link` = ? OR (`parent` != 0 AND `parent` = ? AND `otype` = ?)) AND `uid` = ?', + $note['link'], + $note['parent'], + $note['otype'], + local_user() + ]); } /** @@ -153,11 +126,7 @@ class NotificationsManager extends BaseObject */ public function setAllSeen($seen = true) { - return q( - "UPDATE `notify` SET `seen` = %d WHERE `uid` = %d", - intval($seen), - intval(local_user()) - ); + return DBA::update('notify', ['seen' => $seen], ['uid' => local_user()]); } /** @@ -460,20 +429,22 @@ class NotificationsManager extends BaseObject $notifs = []; $sql_seen = ""; + $filter = ['uid' => local_user()]; if ($seen === 0) { - $sql_seen = " AND NOT `seen` "; + $filter['seen'] = false; } - $r = q( - "SELECT `id`, `url`, `photo`, `msg`, `date`, `seen`, `verb` FROM `notify` - WHERE `uid` = %d $sql_seen ORDER BY `date` DESC LIMIT %d, %d ", - intval(local_user()), - intval($start), - intval($limit) - ); + $params = []; + $params['order'] = ['date' => 'DESC']; + $params['limit'] = [$start, $limit]; - if (DBA::isResult($r)) { - $notifs = $this->formatNotifs($r, $ident); + $stmtNotifies = DBA::select('notify', + ['id', 'url', 'photo', 'msg', 'date', 'seen', 'verb'], + $filter, + $params); + + if (DBA::isResult($stmtNotifies)) { + $notifs = $this->formatNotifs(DBA::toArray($stmtNotifies), $ident); } $arr = [ @@ -596,7 +567,7 @@ class NotificationsManager extends BaseObject } /// @todo Fetch contact details by "Contact::getDetailsByUrl" instead of queries to contact, fcontact and gcontact - $r = q( + $stmtNotifies = DBA::p( "SELECT `intro`.`id` AS `intro_id`, `intro`.*, `contact`.*, `fcontact`.`name` AS `fname`, `fcontact`.`url` AS `furl`, `fcontact`.`addr` AS `faddr`, `fcontact`.`photo` AS `fphoto`, `fcontact`.`request` AS `frequest`, @@ -613,8 +584,8 @@ class NotificationsManager extends BaseObject intval($start), intval($limit) ); - if (DBA::isResult($r)) { - $notifs = $this->formatIntros($r); + if (DBA::isResult($stmtNotifies)) { + $notifs = $this->formatIntros(DBA::toArray($stmtNotifies)); } $arr = [ diff --git a/src/Core/README.md b/src/Core/README.md new file mode 100644 index 0000000000..8a5a3788b5 --- /dev/null +++ b/src/Core/README.md @@ -0,0 +1,4 @@ +## Friendica\Core + +The Core namespace contains classes, which are essential to Friendica. + \ No newline at end of file diff --git a/src/Core/Update.php b/src/Core/Update.php index bb2513d388..0d7b348b42 100644 --- a/src/Core/Update.php +++ b/src/Core/Update.php @@ -2,8 +2,13 @@ namespace Friendica\Core; +use Friendica\App; +use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Database\DBA; use Friendica\Database\DBStructure; +use Friendica\Util\BasePath; +use Friendica\Util\Config\ConfigFileLoader; +use Friendica\Util\Config\ConfigFileSaver; use Friendica\Util\Strings; class Update @@ -24,6 +29,11 @@ class Update return; } + // Don't check the status if the last update was failed + if (Config::get('system', 'update', Update::SUCCESS, true) == Update::FAILED) { + return; + } + $build = Config::get('system', 'build'); if (empty($build)) { @@ -101,7 +111,9 @@ class Update for ($x = $stored + 1; $x <= $current; $x++) { $r = self::runUpdateFunction($x, 'pre_update'); if (!$r) { - break; + Config::set('system', 'update', Update::FAILED); + Lock::release('dbupdate'); + return $r; } } @@ -115,6 +127,7 @@ class Update ); } Logger::error('Update ERROR.', ['from' => $stored, 'to' => $current, 'retval' => $retval]); + Config::set('system', 'update', Update::FAILED); Lock::release('dbupdate'); return $retval; } else { @@ -127,7 +140,9 @@ class Update for ($x = $stored + 1; $x <= $current; $x++) { $r = self::runUpdateFunction($x, 'update'); if (!$r) { - break; + Config::set('system', 'update', Update::FAILED); + Lock::release('dbupdate'); + return $r; } } @@ -136,6 +151,7 @@ class Update self::updateSuccessfull($stored, $current); } + Config::set('system', 'update', Update::SUCCESS); Lock::release('dbupdate'); } } @@ -208,6 +224,93 @@ class Update } } + /** + * Checks the config settings and saves given config values into the config file + * + * @param string $basePath The basepath of Friendica + * @param App\Mode $mode The Application mode + * + * @return bool True, if something has been saved + */ + public static function saveConfigToFile($basePath, App\Mode $mode) + { + $configFileLoader = new ConfigFileLoader($basePath, $mode); + $configCache = new Config\Cache\ConfigCache(); + $configFileLoader->setupCache($configCache, true); + $configFileSaver = new ConfigFileSaver($basePath); + + $updated = false; + + if (self::updateConfigEntry($configCache, $configFileSaver,'config', 'hostname')) { + $updated = true; + }; + + if (self::updateConfigEntry($configCache, $configFileSaver,'system', 'basepath', BasePath::create(dirname(__DIR__) . '/../'))) { + $updated = true; + } + + // In case there is nothing to do, skip the update + if (!$updated) { + return true; + } + + if (!$configFileSaver->saveToConfigFile()) { + Logger::alert('Config entry update failed - maybe wrong permission?'); + return false; + } + + DBA::delete('config', ['cat' => 'config', 'k' => 'hostname']); + DBA::delete('config', ['cat' => 'system', 'k' => 'basepath']); + + return true; + } + + /** + * Adds a value to the ConfigFileSave in case it isn't already updated + * + * @param IConfigCache $configCache The cached config file + * @param ConfigFileSaver $configFileSaver The config file saver + * @param string $cat The config category + * @param string $key The config key + * @param string $default A default value, if none of the settings are valid + * + * @return boolean True, if a value was updated + * + * @throws \Exception if DBA or Logger doesn't work + */ + private static function updateConfigEntry(IConfigCache $configCache, ConfigFileSaver $configFileSaver, $cat, $key, $default = '') + { + // check if the config file differs from the whole configuration (= The db contains other values) + $fileConfig = $configCache->get($cat, $key); + + $savedConfig = DBA::selectFirst('config', ['v'], ['cat' => $cat, 'k' => $key]); + + if (DBA::isResult($savedConfig)) { + $savedValue = $savedConfig['v']; + } else { + $savedValue = null; + } + + // If the db contains a config value, check it + if (isset($savedValue) && $fileConfig !== $savedValue) { + Logger::info('Difference in config found', ['cat' => $cat, 'key' => $key, 'file' => $fileConfig, 'saved' => $savedValue]); + $configFileSaver->addConfigValue($cat, $key, $savedValue); + return true; + + // If both config values are not set, use the default value + } elseif (!isset($fileConfig) && !isset($savedValue)) { + Logger::info('Using default for config', ['cat' => $cat, 'key' => $key, 'value' => $default]); + $configFileSaver->addConfigValue($cat, $key, $default); + return true; + + // If either the file config value isn't empty or the db value is the same as the + // file config value, skip it + } else { + Logger::info('No Difference in config found', ['cat' => $cat, 'key' => $key, 'value' => $fileConfig, 'saved' => $savedValue]); + return false; + } + } + /** * send the email and do what is needed to do on update fails * diff --git a/src/Core/UserImport.php b/src/Core/UserImport.php index 1e103c1f5d..0a4223fecd 100644 --- a/src/Core/UserImport.php +++ b/src/Core/UserImport.php @@ -6,6 +6,7 @@ namespace Friendica\Core; use Friendica\App; use Friendica\Database\DBA; +use Friendica\Database\DBStructure; use Friendica\Model\Photo; use Friendica\Object\Image; use Friendica\Util\Strings; @@ -35,12 +36,11 @@ class UserImport */ private static function checkCols($table, &$arr) { - $query = sprintf("SHOW COLUMNS IN `%s`", DBA::escape($table)); - Logger::log("uimport: $query", Logger::DEBUG); - $r = q($query); + $tableColumns = DBStructure::getColumns($table); + $tcols = []; // get a plain array of column names - foreach ($r as $tcol) { + foreach ($tableColumns as $tcol) { $tcols[] = $tcol['Field']; } // remove inexistent columns @@ -66,16 +66,12 @@ class UserImport } self::checkCols($table, $arr); - $cols = implode("`,`", array_map(['Friendica\Database\DBA', 'escape'], array_keys($arr))); - $vals = implode("','", array_map(['Friendica\Database\DBA', 'escape'], array_values($arr))); - $query = "INSERT INTO `$table` (`$cols`) VALUES ('$vals')"; - Logger::log("uimport: $query", Logger::TRACE); if (self::IMPORT_DEBUG) { return true; } - return q($query); + return DBA::insert($table, $arr); } /** diff --git a/src/Database/DBStructure.php b/src/Database/DBStructure.php index 75a5c86241..abbac4e781 100644 --- a/src/Database/DBStructure.php +++ b/src/Database/DBStructure.php @@ -844,4 +844,18 @@ class DBStructure return $retval; } + + /** + * Returns the columns of a table + * + * @param string $table Table name + * + * @return array An array of the table columns + * @throws Exception + */ + public static function getColumns($table) + { + $stmtColumns = DBA::p("SHOW COLUMNS FROM `" . $table . "`"); + return DBA::toArray($stmtColumns); + } } diff --git a/src/Core/Cache/CacheDriverFactory.php b/src/Factory/CacheDriverFactory.php similarity index 73% rename from src/Core/Cache/CacheDriverFactory.php rename to src/Factory/CacheDriverFactory.php index 307f52a2ea..1008b67944 100644 --- a/src/Core/Cache/CacheDriverFactory.php +++ b/src/Factory/CacheDriverFactory.php @@ -1,8 +1,10 @@ loadConfigFiles($configCache); + $loader->setupCache($configCache); return $configCache; } diff --git a/src/Factory/DependencyFactory.php b/src/Factory/DependencyFactory.php index 9d84e324ab..63defd95f5 100644 --- a/src/Factory/DependencyFactory.php +++ b/src/Factory/DependencyFactory.php @@ -3,9 +3,9 @@ namespace Friendica\Factory; use Friendica\App; -use Friendica\Core\Config\Cache; use Friendica\Factory; use Friendica\Util\BasePath; +use Friendica\Util\Config; class DependencyFactory { @@ -23,16 +23,17 @@ class DependencyFactory public static function setUp($channel, $directory, $isBackend = true) { $basePath = BasePath::create($directory, $_SERVER); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $configLoader = new Config\ConfigFileLoader($basePath, $mode); $configCache = Factory\ConfigFactory::createCache($configLoader); $profiler = Factory\ProfilerFactory::create($configCache); Factory\DBFactory::init($basePath, $configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); // needed to call PConfig::init() Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create($channel, $config); - Factory\LoggerFactory::createDev($channel, $config); + $logger = Factory\LoggerFactory::create($channel, $config, $profiler); + Factory\LoggerFactory::createDev($channel, $config, $profiler); - return new App($basePath, $config, $logger, $profiler, $isBackend); + return new App($config, $mode, $logger, $profiler, $isBackend); } } diff --git a/src/Factory/LoggerFactory.php b/src/Factory/LoggerFactory.php index 81c15bdb5b..444a98cde5 100644 --- a/src/Factory/LoggerFactory.php +++ b/src/Factory/LoggerFactory.php @@ -5,9 +5,13 @@ namespace Friendica\Factory; use Friendica\Core\Config\Configuration; use Friendica\Core\Logger; use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Logger\FriendicaDevelopHandler; -use Friendica\Util\Logger\FriendicaIntrospectionProcessor; -use Friendica\Util\Logger\WorkerLogger; +use Friendica\Util\Introspection; +use Friendica\Util\Logger\Monolog\DevelopHandler; +use Friendica\Util\Logger\Monolog\IntrospectionProcessor; +use Friendica\Util\Logger\ProfilerLogger; +use Friendica\Util\Logger\StreamLogger; +use Friendica\Util\Logger\SyslogLogger; +use Friendica\Util\Logger\VoidLogger; use Friendica\Util\Profiler; use Monolog; use Psr\Log\LoggerInterface; @@ -27,7 +31,7 @@ class LoggerFactory private static $ignoreClassList = [ Logger::class, Profiler::class, - WorkerLogger::class + 'Friendica\\Util\\Logger', ]; /** @@ -35,29 +39,57 @@ class LoggerFactory * * @param string $channel The channel of the logger instance * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws \Exception + * @throws InternalServerErrorException */ - public static function create($channel, Configuration $config) + public static function create($channel, Configuration $config, Profiler $profiler) { - $loggerTimeZone = new \DateTimeZone('UTC'); - Monolog\Logger::setTimezone($loggerTimeZone); + if (empty($config->get('system', 'debugging', false))) { + $logger = new VoidLogger(); + Logger::init($logger); + return $logger; + } - $logger = new Monolog\Logger($channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new FriendicaIntrospectionProcessor(LogLevel::DEBUG, self::$ignoreClassList)); + $introspection = new Introspection(self::$ignoreClassList); + $level = $config->get('system', 'loglevel'); + $loglevel = self::mapLegacyConfigDebugLevel((string)$level); - $debugging = $config->get('system', 'debugging'); - $stream = $config->get('system', 'logfile'); - $level = $config->get('system', 'loglevel'); + switch ($config->get('system', 'logger_config', 'stream')) { + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); - if ($debugging) { - $loglevel = self::mapLegacyConfigDebugLevel((string)$level); - static::addStreamHandler($logger, $stream, $loglevel); - } else { - static::addVoidHandler($logger); + $logger = new Monolog\Logger($channel); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $stream = $config->get('system', 'logfile'); + + static::addStreamHandler($logger, $stream, $loglevel); + break; + + case 'syslog': + $logger = new SyslogLogger($channel, $introspection, $loglevel); + break; + + case 'stream': + default: + $stream = $config->get('system', 'logfile'); + $logger = new StreamLogger($channel, $stream, $introspection, $loglevel); + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); } Logger::init($logger); @@ -75,31 +107,63 @@ class LoggerFactory * * @param string $channel The channel of the logger instance * @param Configuration $config The config + * @param Profiler $profiler The profiler of the app * * @return LoggerInterface The PSR-3 compliant logger instance + * + * @throws InternalServerErrorException + * @throws \Exception */ - public static function createDev($channel, Configuration $config) + public static function createDev($channel, Configuration $config, Profiler $profiler) { $debugging = $config->get('system', 'debugging'); $stream = $config->get('system', 'dlogfile'); $developerIp = $config->get('system', 'dlogip'); if (!isset($developerIp) || !$debugging) { - return null; + $logger = new VoidLogger(); + Logger::setDevLogger($logger); + return $logger; } $loggerTimeZone = new \DateTimeZone('UTC'); Monolog\Logger::setTimezone($loggerTimeZone); - $logger = new Monolog\Logger($channel); - $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); - $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); - $logger->pushProcessor(new Monolog\Processor\UidProcessor()); - $logger->pushProcessor(new FriendicaIntrospectionProcessor(LogLevel::DEBUG, self::$ignoreClassList)); + $introspection = new Introspection(self::$ignoreClassList); - $logger->pushHandler(new FriendicaDevelopHandler($developerIp)); + switch ($config->get('system', 'logger_config', 'stream')) { - static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + case 'monolog': + $loggerTimeZone = new \DateTimeZone('UTC'); + Monolog\Logger::setTimezone($loggerTimeZone); + + $logger = new Monolog\Logger($channel); + $logger->pushProcessor(new Monolog\Processor\PsrLogMessageProcessor()); + $logger->pushProcessor(new Monolog\Processor\ProcessIdProcessor()); + $logger->pushProcessor(new Monolog\Processor\UidProcessor()); + $logger->pushProcessor(new IntrospectionProcessor($introspection, LogLevel::DEBUG)); + + $logger->pushHandler(new DevelopHandler($developerIp)); + + static::addStreamHandler($logger, $stream, LogLevel::DEBUG); + break; + + case 'syslog': + $logger = new SyslogLogger($channel, $introspection, LogLevel::DEBUG); + break; + + case 'stream': + default: + $logger = new StreamLogger($channel, $stream, $introspection, LogLevel::DEBUG); + break; + } + + $profiling = $config->get('system', 'profiling', false); + + // In case profiling is enabled, wrap the ProfilerLogger around the current logger + if (isset($profiling) && $profiling !== false) { + $logger = new ProfilerLogger($logger, $profiler); + } Logger::setDevLogger($logger); diff --git a/src/Factory/README.md b/src/Factory/README.md new file mode 100644 index 0000000000..572d9403c9 --- /dev/null +++ b/src/Factory/README.md @@ -0,0 +1,9 @@ +## Friendica\Factory + +This namespace contains Factories. +A Factory is used to create specific objects based on its configuration. + +See [Factory Method](https://designpatternsphp.readthedocs.io/en/latest/Creational/FactoryMethod/README.html) + +Use the classes inside this directory if you want to change the way how new objects should get created. +Don't use the classes to change the behaviour of the concrete objects. diff --git a/src/Model/Contact.php b/src/Model/Contact.php index d938ad29c2..fe373ae94a 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -109,6 +109,16 @@ class Contact extends BaseObject * @} */ + /** + * @param integer $id + * @return array|boolean Contact record if it exists, false otherwise + * @throws \Exception + */ + public static function getById($id) + { + return DBA::selectFirst('contact', [], ['id' => $id]); + } + /** * @brief Tests if the given contact is a follower * diff --git a/src/Model/README.md b/src/Model/README.md new file mode 100644 index 0000000000..e712d82503 --- /dev/null +++ b/src/Model/README.md @@ -0,0 +1,5 @@ +## Friendica\Model + +Models are the glue between the business logic of the app and the datastore(s). + +In the namespace Model should only be static classes that interact with the DB with the same name as a database table. \ No newline at end of file diff --git a/src/Module/Apps.php b/src/Module/Apps.php new file mode 100644 index 0000000000..efba071aa3 --- /dev/null +++ b/src/Module/Apps.php @@ -0,0 +1,38 @@ +internalRedirect(); + } + } + + public static function content() + { + $apps = Nav::getAppMenu(); + + if (count($apps) == 0) { + notice(L10n::t('No installed applications.') . EOL); + } + + $tpl = Renderer::getMarkupTemplate('apps.tpl'); + return Renderer::replaceMacros($tpl, [ + '$title' => L10n::t('Applications'), + '$apps' => $apps, + ]); + } +} diff --git a/src/Module/Credits.php b/src/Module/Credits.php new file mode 100644 index 0000000000..b0a6545c38 --- /dev/null +++ b/src/Module/Credits.php @@ -0,0 +1,30 @@ + L10n::t('Credits'), + '$thanks' => L10n::t('Friendica is a community project, that would not be possible without the help of many people. Here is a list of those who have contributed to the code or the translation of Friendica. Thank you all!'), + '$names' => $names, + ]); + } +} diff --git a/src/Module/Feedtest.php b/src/Module/Feedtest.php new file mode 100644 index 0000000000..75cca52086 --- /dev/null +++ b/src/Module/Feedtest.php @@ -0,0 +1,53 @@ +internalRedirect(); + } + } + + public static function content() + { + $result = []; + if (!empty($_REQUEST['url'])) { + $url = $_REQUEST['url']; + + $importer = Model\User::getById(local_user()); + + $contact_id = Model\Contact::getIdForURL($url, local_user(), true); + $contact = Model\Contact::getById($contact_id); + + $xml = Network::fetchUrl($contact['poll']); + + $dummy = null; + $import_result = Protocol\Feed::import($xml, $importer, $contact, $dummy, true); + + $result = [ + 'input' => $xml, + 'output' => var_export($import_result, true), + ]; + } + + $tpl = Renderer::getMarkupTemplate('feedtest.tpl'); + return Renderer::replaceMacros($tpl, [ + '$url' => ['url', L10n::t('Source URL'), defaults($_REQUEST, 'url', ''), ''], + '$result' => $result + ]); + } +} diff --git a/src/Module/Filer.php b/src/Module/Filer.php new file mode 100644 index 0000000000..3b7d5175c7 --- /dev/null +++ b/src/Module/Filer.php @@ -0,0 +1,52 @@ +internalRedirect(); + } + } + + public static function content() + { + $a = self::getApp(); + $logger = $a->getLogger(); + + $term = XML::unescape(trim(defaults($_GET, 'term', ''))); + $item_id = (($a->argc > 1) ? intval($a->argv[1]) : 0); + + $logger->info('filer', ['tag' => $term, 'item' => $item_id]); + + if ($item_id && strlen($term)) { + // file item + Model\FileTag::saveFile(local_user(), $item_id, $term); + info(L10n::t('Filetag %s saved to item', $term)); + } + + // return filer dialog + $filetags = PConfig::get(local_user(), 'system', 'filetags'); + $filetags = Model\FileTag::fileToList($filetags, 'file'); + $filetags = explode(",", $filetags); + + $tpl = Renderer::getMarkupTemplate("filer_dialog.tpl"); + return Renderer::replaceMacros($tpl, [ + '$field' => ['term', L10n::t("Save to Folder:"), '', '', $filetags, L10n::t('- select -')], + '$submit' => L10n::t('Save'), + ]); + } +} diff --git a/src/Module/Install.php b/src/Module/Install.php index de19bc86ab..8e93c0a26c 100644 --- a/src/Module/Install.php +++ b/src/Module/Install.php @@ -5,6 +5,7 @@ namespace Friendica\Module; use Friendica\App; use Friendica\BaseModule; use Friendica\Core; +use Friendica\Core\Config\Cache\IConfigCache; use Friendica\Core\L10n; use Friendica\Core\Renderer; use Friendica\Util\Strings; @@ -65,45 +66,48 @@ class Install extends BaseModule public static function post() { $a = self::getApp(); + $configCache = $a->getConfigCache(); switch (self::$currentWizardStep) { case self::SYSTEM_CHECK: case self::DATABASE_CONFIG: - // Nothing to do in these steps + self::checkSetting($configCache, $_POST, 'config', 'php_path'); break; case self::SITE_SETTINGS: - $dbhost = Strings::escapeTags(trim(defaults($_POST, 'dbhost', Core\Installer::DEFAULT_HOST))); - $dbuser = Strings::escapeTags(trim(defaults($_POST, 'dbuser', ''))); - $dbpass = Strings::escapeTags(trim(defaults($_POST, 'dbpass', ''))); - $dbdata = Strings::escapeTags(trim(defaults($_POST, 'dbdata', ''))); + self::checkSetting($configCache, $_POST, 'config', 'php_path'); + + self::checkSetting($configCache, $_POST, 'database', 'hostname', Core\Installer::DEFAULT_HOST); + self::checkSetting($configCache, $_POST, 'database', 'username', ''); + self::checkSetting($configCache, $_POST, 'database', 'password', ''); + self::checkSetting($configCache, $_POST, 'database', 'database', ''); // If we cannot connect to the database, return to the previous step - if (!self::$installer->checkDB($a->getBasePath(), $a->getConfigCache(), $a->getProfiler(), $dbhost, $dbuser, $dbpass, $dbdata)) { + if (!self::$installer->checkDB($a->getBasePath(), $configCache, $a->getProfiler())) { self::$currentWizardStep = self::DATABASE_CONFIG; } break; case self::FINISHED: - $urlpath = $a->getURLPath(); - $dbhost = Strings::escapeTags(trim(defaults($_POST, 'dbhost', Core\Installer::DEFAULT_HOST))); - $dbuser = Strings::escapeTags(trim(defaults($_POST, 'dbuser', ''))); - $dbpass = Strings::escapeTags(trim(defaults($_POST, 'dbpass', ''))); - $dbdata = Strings::escapeTags(trim(defaults($_POST, 'dbdata', ''))); - $timezone = Strings::escapeTags(trim(defaults($_POST, 'timezone', Core\Installer::DEFAULT_TZ))); - $language = Strings::escapeTags(trim(defaults($_POST, 'language', Core\Installer::DEFAULT_LANG))); - $adminmail = Strings::escapeTags(trim(defaults($_POST, 'adminmail', ''))); + self::checkSetting($configCache, $_POST, 'config', 'php_path'); + + self::checkSetting($configCache, $_POST, 'database', 'hostname', Core\Installer::DEFAULT_HOST); + self::checkSetting($configCache, $_POST, 'database', 'username', ''); + self::checkSetting($configCache, $_POST, 'database', 'password', ''); + self::checkSetting($configCache, $_POST, 'database', 'database', ''); + + self::checkSetting($configCache, $_POST, 'system', 'default_timezone', Core\Installer::DEFAULT_TZ); + self::checkSetting($configCache, $_POST, 'system', 'language', Core\Installer::DEFAULT_LANG); + self::checkSetting($configCache, $_POST, 'config', 'admin_email', ''); // If we cannot connect to the database, return to the Database config wizard - if (!self::$installer->checkDB($a->getBasePath(), $a->getConfigCache(), $a->getProfiler(), $dbhost, $dbuser, $dbpass, $dbdata)) { + if (!self::$installer->checkDB($a->getBasePath(), $configCache, $a->getProfiler())) { self::$currentWizardStep = self::DATABASE_CONFIG; return; } - $phpath = self::$installer->getPHPPath(); - - if (!self::$installer->createConfig($phpath, $urlpath, $dbhost, $dbuser, $dbpass, $dbdata, $timezone, $language, $adminmail, $a->getBasePath())) { + if (!self::$installer->createConfig($a, $configCache, $a->getBasePath())) { return; } @@ -116,6 +120,7 @@ class Install extends BaseModule public static function content() { $a = self::getApp(); + $configCache = $a->getConfigCache(); $output = ''; @@ -123,85 +128,62 @@ class Install extends BaseModule switch (self::$currentWizardStep) { case self::SYSTEM_CHECK: - $phppath = defaults($_POST, 'phpath', null); + $php_path = $configCache->get('config', 'php_path'); - $status = self::$installer->checkEnvironment($a->getBaseURL(), $phppath); + $status = self::$installer->checkEnvironment($a->getBaseURL(), $php_path); $tpl = Renderer::getMarkupTemplate('install_checks.tpl'); $output .= Renderer::replaceMacros($tpl, [ - '$title' => $install_title, - '$pass' => L10n::t('System check'), - '$checks' => self::$installer->getChecks(), - '$passed' => $status, - '$see_install' => L10n::t('Please see the file "INSTALL.txt".'), - '$next' => L10n::t('Next'), - '$reload' => L10n::t('Check again'), - '$phpath' => $phppath, - '$baseurl' => $a->getBaseURL() + '$title' => $install_title, + '$pass' => L10n::t('System check'), + '$checks' => self::$installer->getChecks(), + '$passed' => $status, + '$see_install' => L10n::t('Please see the file "INSTALL.txt".'), + '$next' => L10n::t('Next'), + '$reload' => L10n::t('Check again'), + '$php_path' => $php_path, + '$baseurl' => $a->getBaseURL() ]); break; case self::DATABASE_CONFIG: - $dbhost = Strings::escapeTags(trim(defaults($_POST, 'dbhost' , Core\Installer::DEFAULT_HOST))); - $dbuser = Strings::escapeTags(trim(defaults($_POST, 'dbuser' , '' ))); - $dbpass = Strings::escapeTags(trim(defaults($_POST, 'dbpass' , '' ))); - $dbdata = Strings::escapeTags(trim(defaults($_POST, 'dbdata' , '' ))); - $phpath = Strings::escapeTags(trim(defaults($_POST, 'phpath' , '' ))); - $adminmail = Strings::escapeTags(trim(defaults($_POST, 'adminmail', '' ))); - $tpl = Renderer::getMarkupTemplate('install_db.tpl'); $output .= Renderer::replaceMacros($tpl, [ - '$title' => $install_title, - '$pass' => L10n::t('Database connection'), - '$info_01' => L10n::t('In order to install Friendica we need to know how to connect to your database.'), - '$info_02' => L10n::t('Please contact your hosting provider or site administrator if you have questions about these settings.'), - '$info_03' => L10n::t('The database you specify below should already exist. If it does not, please create it before continuing.'), - 'checks' => self::$installer->getChecks(), - '$dbhost' => ['dbhost', + '$title' => $install_title, + '$pass' => L10n::t('Database connection'), + '$info_01' => L10n::t('In order to install Friendica we need to know how to connect to your database.'), + '$info_02' => L10n::t('Please contact your hosting provider or site administrator if you have questions about these settings.'), + '$info_03' => L10n::t('The database you specify below should already exist. If it does not, please create it before continuing.'), + 'checks' => self::$installer->getChecks(), + '$dbhost' => ['database-hostname', L10n::t('Database Server Name'), - $dbhost, + $configCache->get('database', 'hostname'), '', 'required'], - '$dbuser' => ['dbuser', + '$dbuser' => ['database-username', L10n::t('Database Login Name'), - $dbuser, + $configCache->get('database', 'username'), '', 'required', 'autofocus'], - '$dbpass' => ['dbpass', + '$dbpass' => ['database-password', L10n::t('Database Login Password'), - $dbpass, + $configCache->get('database', 'password'), L10n::t("For security reasons the password must not be empty"), 'required'], - '$dbdata' => ['dbdata', + '$dbdata' => ['database-database', L10n::t('Database Name'), - $dbdata, + $configCache->get('database', 'database'), '', 'required'], - '$adminmail' => ['adminmail', - L10n::t('Site administrator email address'), - $adminmail, - L10n::t('Your account email address must match this in order to use the web admin panel.'), - 'required', - 'autofocus', - 'email'], - '$lbl_10' => L10n::t('Please select a default timezone for your website'), - '$baseurl' => $a->getBaseURL(), - '$phpath' => $phpath, - '$submit' => L10n::t('Submit') + '$lbl_10' => L10n::t('Please select a default timezone for your website'), + '$baseurl' => $a->getBaseURL(), + '$php_path' => $configCache->get('config', 'php_path'), + '$submit' => L10n::t('Submit') ]); break; case self::SITE_SETTINGS: - $dbhost = Strings::escapeTags(trim(defaults($_POST, 'dbhost', Core\Installer::DEFAULT_HOST))); - $dbuser = Strings::escapeTags(trim(defaults($_POST, 'dbuser', '' ))); - $dbpass = Strings::escapeTags(trim(defaults($_POST, 'dbpass', '' ))); - $dbdata = Strings::escapeTags(trim(defaults($_POST, 'dbdata', '' ))); - $phpath = Strings::escapeTags(trim(defaults($_POST, 'phpath', '' ))); - - $adminmail = Strings::escapeTags(trim(defaults($_POST, 'adminmail', ''))); - - $timezone = defaults($_POST, 'timezone', Core\Installer::DEFAULT_TZ); /* Installed langs */ $lang_choices = L10n::getAvailableLanguages(); @@ -210,16 +192,23 @@ class Install extends BaseModule '$title' => $install_title, '$checks' => self::$installer->getChecks(), '$pass' => L10n::t('Site settings'), - '$dbhost' => $dbhost, - '$dbuser' => $dbuser, - '$dbpass' => $dbpass, - '$dbdata' => $dbdata, - '$phpath' => $phpath, - '$adminmail' => ['adminmail', L10n::t('Site administrator email address'), $adminmail, L10n::t('Your account email address must match this in order to use the web admin panel.'), 'required', 'autofocus', 'email'], - '$timezone' => Temporal::getTimezoneField('timezone', L10n::t('Please select a default timezone for your website'), $timezone, ''), - '$language' => ['language', + '$dbhost' => $configCache->get('database', 'hostname'), + '$dbuser' => $configCache->get('database', 'username'), + '$dbpass' => $configCache->get('database', 'password'), + '$dbdata' => $configCache->get('database', 'database'), + '$phpath' => $configCache->get('config', 'php_path'), + '$adminmail' => ['config-admin_email', + L10n::t('Site administrator email address'), + $configCache->get('config', 'admin_email'), + L10n::t('Your account email address must match this in order to use the web admin panel.'), + 'required', 'autofocus', 'email'], + '$timezone' => Temporal::getTimezoneField('system-default_timezone', + L10n::t('Please select a default timezone for your website'), + $configCache->get('system', 'default_timezone'), + ''), + '$language' => ['system-language', L10n::t('System Language:'), - Core\Installer::DEFAULT_LANG, + $configCache->get('system', 'language'), L10n::t('Set the default language for your Friendica installation interface and to send emails.'), $lang_choices], '$baseurl' => $a->getBaseURL(), @@ -269,4 +258,24 @@ class Install extends BaseModule . L10n::t('Go to your new Friendica node registration page and register as new user. Remember to use the same email you have entered as administrator email. This will allow you to enter the site admin panel.', $baseurl) . "

"; } + + /** + * Checks the $_POST settings and updates the config Cache for it + * + * @param IConfigCache $configCache The current config cache + * @param array $post The $_POST data + * @param string $cat The category of the setting + * @param string $key The key of the setting + * @param null|string $default The default value + */ + private static function checkSetting(IConfigCache $configCache, array $post, $cat, $key, $default = null) + { + $configCache->set($cat, $key, + Strings::escapeTags( + trim(defaults($post, sprintf('%s-%s', $cat, $key), + (!isset($default) ? $configCache->get($cat, $key) : $default)) + ) + ) + ); + } } diff --git a/src/Module/README.md b/src/Module/README.md new file mode 100644 index 0000000000..f888f26326 --- /dev/null +++ b/src/Module/README.md @@ -0,0 +1,10 @@ +## Friendica\Module + +The Module namespace contains the different modules of Friendica. +Each module is loaded through the [`App`](https://github.com/friendica/friendica/blob/develop/src/App.php). + +Rules for Modules: +- Named like the call (i.e. https://friendica.test/contact => `Contact`) +- Start with capitals and are **not** camelCased. +- Directly interacting with a given request (POST or GET) +- Extending [`BaseModule`](https://github.com/friendica/friendica/blob/develop/src/BaseModule.php). \ No newline at end of file diff --git a/src/Object/README.md b/src/Object/README.md new file mode 100644 index 0000000000..155bc5d0cb --- /dev/null +++ b/src/Object/README.md @@ -0,0 +1,5 @@ +## Friendica\Object + +The namespace Object contains dynamic classes which are **not** directly interacting with the datastore. + +They are used to implement business logic for a particular object (i.e. an Image). \ No newline at end of file diff --git a/src/Core/Config/Cache/ConfigCacheLoader.php b/src/Util/Config/ConfigFileLoader.php similarity index 75% rename from src/Core/Config/Cache/ConfigCacheLoader.php rename to src/Util/Config/ConfigFileLoader.php index 55f18681ce..3d4f31a9d1 100644 --- a/src/Core/Config/Cache/ConfigCacheLoader.php +++ b/src/Util/Config/ConfigFileLoader.php @@ -1,41 +1,44 @@ baseDir = $baseDir; - $this->configDir = $baseDir . DIRECTORY_SEPARATOR . self::SUBDIRECTORY; + parent::__construct($baseDir); + $this->appMode = $mode; } /** - * Load the configuration files + * Load the configuration files into an configuration cache * * First loads the default value for all the configuration keys, then the legacy configuration files, then the * expected local.config.php + * + * @param IConfigCache $config The config cache to load to + * @param bool $raw Setup the raw config format + * + * @throws \Exception */ - public function loadConfigFiles(ConfigCache $config) + public function setupCache(IConfigCache $config, $raw = false) { $config->load($this->loadCoreConfig('defaults')); $config->load($this->loadCoreConfig('settings')); @@ -44,23 +47,29 @@ class ConfigCacheLoader $config->load($this->loadLegacyConfig('htconfig'), true); $config->load($this->loadCoreConfig('local'), true); + + // In case of install mode, add the found basepath (because there isn't a basepath set yet + if (!$raw && ($this->appMode->isInstall() || empty($config->get('system', 'basepath')))) { + // Setting at least the basepath we know + $config->set('system', 'basepath', $this->baseDir); + } } /** * Tries to load the specified core-configuration and returns the config array. * - * @param string $name The name of the configuration + * @param string $name The name of the configuration (default is empty, which means 'local') * * @return array The config array (empty if no config found) * * @throws \Exception if the configuration file isn't readable */ - public function loadCoreConfig($name) + public function loadCoreConfig($name = '') { - if (file_exists($this->configDir . DIRECTORY_SEPARATOR . $name . '.config.php')) { - return $this->loadConfigFile($this->configDir . DIRECTORY_SEPARATOR . $name . '.config.php'); - } elseif (file_exists($this->configDir . DIRECTORY_SEPARATOR . $name . '.ini.php')) { - return $this->loadINIConfigFile($this->configDir . DIRECTORY_SEPARATOR . $name . '.ini.php'); + if (!empty($this->getConfigFullName($name))) { + return $this->loadConfigFile($this->getConfigFullName($name)); + } elseif (!empty($this->getIniFullName($name))) { + return $this->loadINIConfigFile($this->getIniFullName($name)); } else { return []; } @@ -93,22 +102,19 @@ class ConfigCacheLoader /** * Tries to load the legacy config files (.htconfig.php, .htpreconfig.php) and returns the config array. * - * @param string $name The name of the config file + * @param string $name The name of the config file (default is empty, which means .htconfig.php) * * @return array The configuration array (empty if no config found) * * @deprecated since version 2018.09 */ - private function loadLegacyConfig($name) + private function loadLegacyConfig($name = '') { - $filePath = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php'; - $config = []; - - if (file_exists($filePath)) { + if (!empty($this->getHtConfigFullName($name))) { $a = new \stdClass(); $a->config = []; - include $filePath; + include $this->getHtConfigFullName($name); $htConfigCategories = array_keys($a->config); diff --git a/src/Util/Config/ConfigFileManager.php b/src/Util/Config/ConfigFileManager.php new file mode 100644 index 0000000000..729e59d746 --- /dev/null +++ b/src/Util/Config/ConfigFileManager.php @@ -0,0 +1,90 @@ +baseDir = $baseDir; + $this->configDir = $baseDir . DIRECTORY_SEPARATOR . self::SUBDIRECTORY; + } + + /** + * Gets the full name (including the path) for a *.config.php (default is local.config.php) + * + * @param string $name The config name (default is empty, which means local.config.php) + * + * @return string The full name or empty if not found + */ + protected function getConfigFullName($name = '') + { + $name = !empty($name) ? $name : self::CONFIG_LOCAL; + + $fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.config.php'; + return file_exists($fullName) ? $fullName : ''; + } + + /** + * Gets the full name (including the path) for a *.ini.php (default is local.ini.php) + * + * @param string $name The config name (default is empty, which means local.ini.php) + * + * @return string The full name or empty if not found + */ + protected function getIniFullName($name = '') + { + $name = !empty($name) ? $name : self::CONFIG_INI; + + $fullName = $this->configDir . DIRECTORY_SEPARATOR . $name . '.ini.php'; + return file_exists($fullName) ? $fullName : ''; + } + + /** + * Gets the full name (including the path) for a .*.php (default is .htconfig.php) + * + * @param string $name The config name (default is empty, which means .htconfig.php) + * + * @return string The full name or empty if not found + */ + protected function getHtConfigFullName($name = '') + { + $name = !empty($name) ? $name : self::CONFIG_HTCONFIG; + + $fullName = $this->baseDir . DIRECTORY_SEPARATOR . '.' . $name . '.php'; + return file_exists($fullName) ? $fullName : ''; + } +} diff --git a/src/Util/Config/ConfigFileSaver.php b/src/Util/Config/ConfigFileSaver.php new file mode 100644 index 0000000000..e32e11a4d5 --- /dev/null +++ b/src/Util/Config/ConfigFileSaver.php @@ -0,0 +1,341 @@ +settings)); + + for ($i = 0; $i < $settingsCount; $i++) { + // if already set, overwrite the value + if ($this->settings[$i]['cat'] === $cat && + $this->settings[$i]['key'] === $key) { + $this->settings[$i] = ['cat' => $cat, 'key' => $key, 'value' => $value]; + return; + } + } + + $this->settings[] = ['cat' => $cat, 'key' => $key, 'value' => $value]; + } + + /** + * Resetting all added configuration entries so far + */ + public function reset() + { + $this->settings = []; + } + + /** + * Save all added configuration entries to the given config files + * After updating the config entries, all configuration entries will be reseted + * + * @param string $name The name of the configuration file (default is empty, which means the default name each type) + * + * @return bool true, if at least one configuration file was successfully updated or nothing to do + */ + public function saveToConfigFile($name = '') + { + // If no settings et, return true + if (count(array_keys($this->settings)) === 0) { + return true; + } + + $saved = false; + + // Check for the *.config.php file inside the /config/ path + list($reading, $writing) = $this->openFile($this->getConfigFullName($name)); + if (isset($reading) && isset($writing)) { + $this->saveConfigFile($reading, $writing); + // Close the current file handler and rename them + if ($this->closeFile($this->getConfigFullName($name), $reading, $writing)) { + // just return true, if everything went fine + $saved = true; + } + } + + // Check for the *.ini.php file inside the /config/ path + list($reading, $writing) = $this->openFile($this->getIniFullName($name)); + if (isset($reading) && isset($writing)) { + $this->saveINIConfigFile($reading, $writing); + // Close the current file handler and rename them + if ($this->closeFile($this->getIniFullName($name), $reading, $writing)) { + // just return true, if everything went fine + $saved = true; + } + } + + // Check for the *.php file (normally .htconfig.php) inside the / path + list($reading, $writing) = $this->openFile($this->getHtConfigFullName($name)); + if (isset($reading) && isset($writing)) { + $this->saveToLegacyConfig($reading, $writing); + // Close the current file handler and rename them + if ($this->closeFile($this->getHtConfigFullName($name), $reading, $writing)) { + // just return true, if everything went fine + $saved = true; + } + } + + $this->reset(); + + return $saved; + } + + /** + * Opens a config file and returns two handler for reading and writing + * + * @param string $fullName The full name of the current config + * + * @return array An array containing the two reading and writing handler + */ + private function openFile($fullName) + { + if (empty($fullName)) { + return [null, null]; + } + + try { + $reading = fopen($fullName, 'r'); + } catch (\Exception $exception) { + return [null, null]; + } + + if (!$reading) { + return [null, null]; + } + + try { + $writing = fopen($fullName . '.tmp', 'w'); + } catch (\Exception $exception) { + fclose($reading); + return [null, null]; + } + + if (!$writing) { + fclose($reading); + return [null, null]; + } + + return [$reading, $writing]; + } + + /** + * Close and rename the config file + * + * @param string $fullName The full name of the current config + * @param resource $reading The reading resource handler + * @param resource $writing The writing resource handler + * + * @return bool True, if the close was successful + */ + private function closeFile($fullName, $reading, $writing) + { + fclose($reading); + fclose($writing); + + try { + $renamed = rename($fullName, $fullName . '.old'); + } catch (\Exception $exception) { + return false; + } + + if (!$renamed) { + return false; + } + + try { + $renamed = rename($fullName . '.tmp', $fullName); + } catch (\Exception $exception) { + // revert the move of the current config file to have at least the old config + rename($fullName . '.old', $fullName); + return false; + } + + if (!$renamed) { + // revert the move of the current config file to have at least the old config + rename($fullName . '.old', $fullName); + return false; + } + + return true; + } + + /** + * Saves all configuration values to a config file + * + * @param resource $reading The reading handler + * @param resource $writing The writing handler + */ + private function saveConfigFile($reading, $writing) + { + $settingsCount = count(array_keys($this->settings)); + $categoryFound = array_fill(0, $settingsCount, false); + $categoryBracketFound = array_fill(0, $settingsCount, false);; + $lineFound = array_fill(0, $settingsCount, false);; + $lineArrowFound = array_fill(0, $settingsCount, false);; + + while (!feof($reading)) { + + $line = fgets($reading); + + // check for each added setting if we have to replace a config line + for ($i = 0; $i < $settingsCount; $i++) { + + // find the first line like "'system' =>" + if (!$categoryFound[$i] && stristr($line, sprintf('\'%s\'', $this->settings[$i]['cat']))) { + $categoryFound[$i] = true; + } + + // find the first line with a starting bracket ( "[" ) + if ($categoryFound[$i] && !$categoryBracketFound[$i] && stristr($line, '[')) { + $categoryBracketFound[$i] = true; + } + + // find the first line with the key like "'value'" + if ($categoryBracketFound[$i] && !$lineFound[$i] && stristr($line, sprintf('\'%s\'', $this->settings[$i]['key']))) { + $lineFound[$i] = true; + } + + // find the first line with an arrow ("=>") after finding the key + if ($lineFound[$i] && !$lineArrowFound[$i] && stristr($line, '=>')) { + $lineArrowFound[$i] = true; + } + + // find the current value and replace it + if ($lineArrowFound[$i] && preg_match_all('/\'(.*?)\'/', $line, $matches, PREG_SET_ORDER)) { + $lineVal = end($matches)[0]; + $line = str_replace($lineVal, '\'' . $this->settings[$i]['value'] . '\'', $line); + $categoryFound[$i] = false; + $categoryBracketFound[$i] = false; + $lineFound[$i] = false; + $lineArrowFound[$i] = false; + // if a line contains a closing bracket for the category ( "]" ) and we didn't find the key/value pair, + // add it as a new line before the closing bracket + } elseif ($categoryBracketFound[$i] && !$lineArrowFound[$i] && stristr($line, ']')) { + $categoryFound[$i] = false; + $categoryBracketFound[$i] = false; + $lineFound[$i] = false; + $lineArrowFound[$i] = false; + $newLine = sprintf(self::INDENT . self::INDENT . '\'%s\' => \'%s\',' . PHP_EOL, $this->settings[$i]['key'], $this->settings[$i]['value']); + $line = $newLine . $line; + } + } + + fputs($writing, $line); + } + } + + /** + * Saves a value to a ini file + * + * @param resource $reading The reading handler + * @param resource $writing The writing handler + */ + private function saveINIConfigFile($reading, $writing) + { + $settingsCount = count(array_keys($this->settings)); + $categoryFound = array_fill(0, $settingsCount, false); + + while (!feof($reading)) { + + $line = fgets($reading); + + // check for each added setting if we have to replace a config line + for ($i = 0; $i < $settingsCount; $i++) { + + // find the category of the current setting + if (!$categoryFound[$i] && stristr($line, sprintf('[%s]', $this->settings[$i]['cat']))) { + $categoryFound[$i] = true; + + // check the current value + } elseif ($categoryFound[$i] && preg_match_all('/^' . $this->settings[$i]['key'] . '\s*=\s*(.*?)$/', $line, $matches, PREG_SET_ORDER)) { + $line = $this->settings[$i]['key'] . ' = ' . $this->settings[$i]['value'] . PHP_EOL; + $categoryFound[$i] = false; + + // If end of INI file, add the line before the INI end + } elseif ($categoryFound[$i] && (preg_match_all('/^\[.*?\]$/', $line) || preg_match_all('/^INI;.*$/', $line))) { + $categoryFound[$i] = false; + $newLine = $this->settings[$i]['key'] . ' = ' . $this->settings[$i]['value'] . PHP_EOL; + $line = $newLine . $line; + } + } + + fputs($writing, $line); + } + } + + /** + * Saves a value to a .php file (normally .htconfig.php) + * + * @param resource $reading The reading handler + * @param resource $writing The writing handler + */ + private function saveToLegacyConfig($reading, $writing) + { + $settingsCount = count(array_keys($this->settings)); + $found = array_fill(0, $settingsCount, false); + while (!feof($reading)) { + + $line = fgets($reading); + + // check for each added setting if we have to replace a config line + for ($i = 0; $i < $settingsCount; $i++) { + + // check for a non plain config setting (use category too) + if ($this->settings[$i]['cat'] !== 'config' && preg_match_all('/^\$a\-\>config\[\'' . $this->settings[$i]['cat'] . '\'\]\[\'' . $this->settings[$i]['key'] . '\'\]\s*=\s\'*(.*?)\';$/', $line, $matches, PREG_SET_ORDER)) { + $line = '$a->config[\'' . $this->settings[$i]['cat'] . '\'][\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL; + $found[$i] = true; + + // check for a plain config setting (don't use a category) + } elseif ($this->settings[$i]['cat'] === 'config' && preg_match_all('/^\$a\-\>config\[\'' . $this->settings[$i]['key'] . '\'\]\s*=\s\'*(.*?)\';$/', $line, $matches, PREG_SET_ORDER)) { + $line = '$a->config[\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL; + $found[$i] = true; + } + } + + fputs($writing, $line); + } + + for ($i = 0; $i < $settingsCount; $i++) { + if (!$found[$i]) { + if ($this->settings[$i]['cat'] !== 'config') { + $line = '$a->config[\'' . $this->settings[$i]['cat'] . '\'][\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL; + } else { + $line = '$a->config[\'' . $this->settings[$i]['key'] . '\'] = \'' . $this->settings[$i]['value'] . '\';' . PHP_EOL; + } + + fputs($writing, $line); + } + } + } +} diff --git a/src/Util/Introspection.php b/src/Util/Introspection.php new file mode 100644 index 0000000000..6eec70954d --- /dev/null +++ b/src/Util/Introspection.php @@ -0,0 +1,88 @@ +skipClassesPartials = $skipClassesPartials; + $this->skipStackFramesCount = $skipStackFramesCount; + } + + /** + * Adds new classes to get skipped + * @param array $classNames + */ + public function addClasses(array $classNames) + { + $this->skipClassesPartials = array_merge($this->skipClassesPartials, $classNames); + } + + /** + * Returns the introspection record of the current call + * + * @return array + */ + public function getRecord() + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + $i = 1; + + while ($this->isTraceClassOrSkippedFunction($trace, $i)) { + $i++; + } + + $i += $this->skipStackFramesCount; + + return [ + 'file' => isset($trace[$i - 1]['file']) ? basename($trace[$i - 1]['file']) : null, + 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, + 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, + ]; + } + + /** + * Checks if the current trace class or function has to be skipped + * + * @param array $trace The current trace array + * @param int $index The index of the current hierarchy level + * + * @return bool True if the class or function should get skipped, otherwise false + */ + private function isTraceClassOrSkippedFunction(array $trace, $index) + { + if (!isset($trace[$index])) { + return false; + } + + if (isset($trace[$index]['class'])) { + foreach ($this->skipClassesPartials as $part) { + if (strpos($trace[$index]['class'], $part) !== false) { + return true; + } + } + } elseif (in_array($trace[$index]['function'], $this->skipFunctions)) { + return true; + } + + return false; + } +} diff --git a/src/Util/Logger/AbstractLogger.php b/src/Util/Logger/AbstractLogger.php new file mode 100644 index 0000000000..576f4bfb43 --- /dev/null +++ b/src/Util/Logger/AbstractLogger.php @@ -0,0 +1,158 @@ +channel = $channel; + $this->introspection = $introspection; + $this->logUid = Strings::getRandomHex(6); + } + + /** + * Simple interpolation of PSR-3 compliant replacements ( variables between '{' and '}' ) + * @see https://www.php-fig.org/psr/psr-3/#12-message + * + * @param string $message + * @param array $context + * + * @return string the interpolated message + */ + protected function psrInterpolate($message, array $context = array()) + { + $replace = []; + foreach ($context as $key => $value) { + // check that the value can be casted to string + if (!is_array($value) && (!is_object($value) || method_exists($value, '__toString'))) { + $replace['{' . $key . '}'] = $value; + } elseif (is_array($value)) { + $replace['{' . $key . '}'] = @json_encode($value); + } + } + + return strtr($message, $replace); + } + + /** + * {@inheritdoc} + */ + public function emergency($message, array $context = array()) + { + $this->addEntry(LogLevel::EMERGENCY, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function alert($message, array $context = array()) + { + $this->addEntry(LogLevel::ALERT, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function critical($message, array $context = array()) + { + $this->addEntry(LogLevel::CRITICAL, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function error($message, array $context = array()) + { + $this->addEntry(LogLevel::ERROR, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function warning($message, array $context = array()) + { + $this->addEntry(LogLevel::WARNING, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function notice($message, array $context = array()) + { + $this->addEntry(LogLevel::NOTICE, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function info($message, array $context = array()) + { + $this->addEntry(LogLevel::INFO, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function debug($message, array $context = array()) + { + $this->addEntry(LogLevel::DEBUG, (string) $message, $context); + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = array()) + { + $this->addEntry($level, (string) $message, $context); + } +} diff --git a/src/Util/Logger/FriendicaIntrospectionProcessor.php b/src/Util/Logger/FriendicaIntrospectionProcessor.php deleted file mode 100644 index aa3933a215..0000000000 --- a/src/Util/Logger/FriendicaIntrospectionProcessor.php +++ /dev/null @@ -1,94 +0,0 @@ -level = Logger::toMonologLevel($level); - $this->skipClassesPartials = array_merge(array('Monolog\\'), $skipClassesPartials); - $this->skipStackFramesCount = $skipStackFramesCount; - } - - public function __invoke(array $record) - { - // return if the level is not high enough - if ($record['level'] < $this->level) { - return $record; - } - - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); - - $i = 1; - - while ($this->isTraceClassOrSkippedFunction($trace, $i)) { - $i++; - } - - $i += $this->skipStackFramesCount; - - // we should have the call source now - $record['extra'] = array_merge( - $record['extra'], - [ - 'file' => isset($trace[$i - 1]['file']) ? basename($trace[$i - 1]['file']) : null, - 'line' => isset($trace[$i - 1]['line']) ? $trace[$i - 1]['line'] : null, - 'function' => isset($trace[$i]['function']) ? $trace[$i]['function'] : null, - ] - ); - - return $record; - } - - /** - * Checks if the current trace class or function has to be skipped - * - * @param array $trace The current trace array - * @param int $index The index of the current hierarchy level - * @return bool True if the class or function should get skipped, otherwise false - */ - private function isTraceClassOrSkippedFunction(array $trace, $index) - { - if (!isset($trace[$index])) { - return false; - } - - if (isset($trace[$index]['class'])) { - foreach ($this->skipClassesPartials as $part) { - if (strpos($trace[$index]['class'], $part) !== false) { - return true; - } - } - } elseif (in_array($trace[$index]['function'], $this->skipFunctions)) { - return true; - } - - return false; - } -} diff --git a/src/Util/Logger/FriendicaDevelopHandler.php b/src/Util/Logger/Monolog/DevelopHandler.php similarity index 93% rename from src/Util/Logger/FriendicaDevelopHandler.php rename to src/Util/Logger/Monolog/DevelopHandler.php index 908d7052cc..07a839345a 100644 --- a/src/Util/Logger/FriendicaDevelopHandler.php +++ b/src/Util/Logger/Monolog/DevelopHandler.php @@ -1,6 +1,6 @@ level = Logger::toMonologLevel($level); + $introspection->addClasses(array('Monolog\\')); + $this->introspection = $introspection; + } + + public function __invoke(array $record) + { + // return if the level is not high enough + if ($record['level'] < $this->level) { + return $record; + } + // we should have the call source now + $record['extra'] = array_merge( + $record['extra'], + $this->introspection->getRecord() + ); + + return $record; + } +} diff --git a/src/Util/Logger/ProfilerLogger.php b/src/Util/Logger/ProfilerLogger.php new file mode 100644 index 0000000000..fdb7c52b2a --- /dev/null +++ b/src/Util/Logger/ProfilerLogger.php @@ -0,0 +1,127 @@ +logger = $logger; + $this->profiler = $profiler; + } + + /** + * {@inheritdoc} + */ + public function emergency($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->emergency($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function alert($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->alert($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function critical($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->critical($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function error($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->error($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function warning($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->warning($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function notice($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->notice($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function info($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->info($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function debug($message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->debug($message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } + + /** + * {@inheritdoc} + */ + public function log($level, $message, array $context = array()) + { + $stamp1 = microtime(true); + $this->logger->log($level, $message, $context); + $this->profiler->saveTimestamp($stamp1, 'file', System::callstack()); + } +} diff --git a/src/Util/Logger/README.md b/src/Util/Logger/README.md new file mode 100644 index 0000000000..449403194d --- /dev/null +++ b/src/Util/Logger/README.md @@ -0,0 +1,27 @@ +## Friendica\Util\Logger + +This namespace contains the different implementations of a Logger. + +### Configuration guideline + +The following settings are possible for `logger_config`: +- `monolog`: A Logging framework with lots of additions (see [Monolog](https://github.com/Seldaek/monolog/)). There are just Friendica additions inside the Monolog directory +- [`stream`](StreamLogger.php): A small logger for files or streams +- [`syslog`](SyslogLogger.php): Prints the logging output into the syslog + +[`VoidLogger`](VoidLogger.php) is a fallback logger without any function if no debugging is enabled. + +[`ProfilerLogger`](ProfilerLogger.php) is a wrapper around an existing logger in case profiling is enabled for Friendica. +Every log call will be saved to the `Profiler` with a timestamp. + +### Implementation guideline + +Each logging implementation should pe capable of printing at least the following information: +- An unique ID for each Request/Call +- The process ID (PID) +- A timestamp of the logging entry +- The critically of the log entry +- A log message +- A context of the log message (f.e which user) + +If possible, a Logger should extend [`AbstractLogger`](AbstractLogger.php), because it contains additional, Friendica specific business logic for each logging call. diff --git a/src/Util/Logger/StreamLogger.php b/src/Util/Logger/StreamLogger.php new file mode 100644 index 0000000000..3031461061 --- /dev/null +++ b/src/Util/Logger/StreamLogger.php @@ -0,0 +1,197 @@ + 0, + LogLevel::ALERT => 1, + LogLevel::CRITICAL => 2, + LogLevel::ERROR => 3, + LogLevel::WARNING => 4, + LogLevel::NOTICE => 5, + LogLevel::INFO => 6, + LogLevel::DEBUG => 7, + ]; + + /** + * {@inheritdoc} + * @param string|resource $stream The stream to write with this logger (either a file or a stream, i.e. stdout) + * @param string $level The minimum loglevel at which this logger will be triggered + * + * @throws \Exception + */ + public function __construct($channel, $stream, Introspection $introspection, $level = LogLevel::DEBUG) + { + parent::__construct($channel, $introspection); + + if (is_resource($stream)) { + $this->stream = $stream; + } elseif (is_string($stream)) { + $this->url = $stream; + } else { + throw new \InvalidArgumentException('A stream must either be a resource or a string.'); + } + + $this->pid = getmypid(); + if (array_key_exists($level, $this->levelToInt)) { + $this->logLevel = $this->levelToInt[$level]; + } else { + throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + } + + public function close() + { + if ($this->url && is_resource($this->stream)) { + fclose($this->stream); + } + + $this->stream = null; + } + + /** + * Adds a new entry to the log + * + * @param int $level + * @param string $message + * @param array $context + * + * @return void + */ + protected function addEntry($level, $message, $context = []) + { + if (!array_key_exists($level, $this->levelToInt)) { + throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + + $logLevel = $this->levelToInt[$level]; + + if ($logLevel > $this->logLevel) { + return; + } + + $this->checkStream(); + + $formattedLog = $this->formatLog($level, $message, $context); + fwrite($this->stream, $formattedLog); + } + + /** + * Formats a log record for the syslog output + * + * @param int $level The loglevel/priority + * @param string $message The message + * @param array $context The context of this call + * + * @return string the formatted syslog output + */ + private function formatLog($level, $message, $context = []) + { + $record = $this->introspection->getRecord(); + $record = array_merge($record, ['uid' => $this->logUid, 'process_id' => $this->pid]); + $logMessage = ''; + + $logMessage .= DateTimeFormat::utcNow() . ' '; + $logMessage .= $this->channel . ' '; + $logMessage .= '[' . strtoupper($level) . ']: '; + $logMessage .= $this->psrInterpolate($message, $context) . ' '; + $logMessage .= @json_encode($context) . ' - '; + $logMessage .= @json_encode($record); + $logMessage .= PHP_EOL; + + return $logMessage; + } + + private function checkStream() + { + if (is_resource($this->stream)) { + return; + } + + if (empty($this->url)) { + throw new \LogicException('Missing stream URL.'); + } + + $this->createDir(); + set_error_handler([$this, 'customErrorHandler']); + $this->stream = fopen($this->url, 'ab'); + restore_error_handler(); + + if (!is_resource($this->stream)) { + $this->stream = null; + + throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: ' . $this->errorMessage, $this->url)); + } + } + + private function createDir() + { + $dirname = null; + $pos = strpos($this->url, '://'); + if (!$pos) { + $dirname = dirname($this->url); + } + + if (substr($this->url, 0, 7) === 'file://') { + $dirname = dirname(substr($this->url, 7)); + } + + if (isset($dirname) && !is_dir($dirname)) { + set_error_handler([$this, 'customErrorHandler']); + $status = mkdir($dirname, 0777, true); + restore_error_handler(); + + if (!$status && !is_dir($dirname)) { + throw new \UnexpectedValueException(sprintf('Directory "%s" cannot get created: ' . $this->errorMessage, $dirname)); + } + } + } + + private function customErrorHandler($code, $msg) + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + } +} diff --git a/src/Util/Logger/SyslogLogger.php b/src/Util/Logger/SyslogLogger.php new file mode 100644 index 0000000000..83c3fc3ce5 --- /dev/null +++ b/src/Util/Logger/SyslogLogger.php @@ -0,0 +1,206 @@ + LOG_DEBUG, + LogLevel::INFO => LOG_INFO, + LogLevel::NOTICE => LOG_NOTICE, + LogLevel::WARNING => LOG_WARNING, + LogLevel::ERROR => LOG_ERR, + LogLevel::CRITICAL => LOG_CRIT, + LogLevel::ALERT => LOG_ALERT, + LogLevel::EMERGENCY => LOG_EMERG, + ]; + + /** + * Translates log priorities to string outputs + * @var array + */ + private $logToString = [ + LOG_DEBUG => 'DEBUG', + LOG_INFO => 'INFO', + LOG_NOTICE => 'NOTICE', + LOG_WARNING => 'WARNING', + LOG_ERR => 'ERROR', + LOG_CRIT => 'CRITICAL', + LOG_ALERT => 'ALERT', + LOG_EMERG => 'EMERGENCY' + ]; + + /** + * Indicates what logging options will be used when generating a log message + * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters + * + * @var int + */ + private $logOpts; + + /** + * Used to specify what type of program is logging the message + * @see http://php.net/manual/en/function.openlog.php#refsect1-function.openlog-parameters + * + * @var int + */ + private $logFacility; + + /** + * The minimum loglevel at which this logger will be triggered + * @var int + */ + private $logLevel; + + /** + * A error message of the current operation + * @var string + */ + private $errorMessage; + + /** + * {@inheritdoc} + * @param string $level The minimum loglevel at which this logger will be triggered + * @param int $logOpts Indicates what logging options will be used when generating a log message + * @param int $logFacility Used to specify what type of program is logging the message + * + * @throws \Exception + */ + public function __construct($channel, Introspection $introspection, $level = LogLevel::NOTICE, $logOpts = LOG_PID, $logFacility = LOG_USER) + { + parent::__construct($channel, $introspection); + $this->logOpts = $logOpts; + $this->logFacility = $logFacility; + $this->logLevel = $this->mapLevelToPriority($level); + $this->introspection->addClasses(array(self::class)); + } + + /** + * Adds a new entry to the syslog + * + * @param int $level + * @param string $message + * @param array $context + * + * @throws InternalServerErrorException if the syslog isn't available + */ + protected function addEntry($level, $message, $context = []) + { + $logLevel = $this->mapLevelToPriority($level); + + if ($logLevel > $this->logLevel) { + return; + } + + $formattedLog = $this->formatLog($logLevel, $message, $context); + $this->write($logLevel, $formattedLog); + } + + /** + * Maps the LogLevel (@see LogLevel ) to a SysLog priority (@see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters ) + * + * @param string $level A LogLevel + * + * @return int The SysLog priority + * + * @throws \Psr\Log\InvalidArgumentException If the loglevel isn't valid + */ + public function mapLevelToPriority($level) + { + if (!array_key_exists($level, $this->logLevels)) { + throw new \InvalidArgumentException(sprintf('The level "%s" is not valid.', $level)); + } + + return $this->logLevels[$level]; + } + + /** + * Closes the Syslog + */ + public function close() + { + closelog(); + } + + /** + * Writes a message to the syslog + * @see http://php.net/manual/en/function.syslog.php#refsect1-function.syslog-parameters + * + * @param int $priority The Priority + * @param string $message The message of the log + * + * @throws InternalServerErrorException if syslog cannot be used + */ + private function write($priority, $message) + { + set_error_handler([$this, 'customErrorHandler']); + $opened = openlog(self::IDENT, $this->logOpts, $this->logFacility); + restore_error_handler(); + + if (!$opened) { + throw new \UnexpectedValueException(sprintf('Can\'t open syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); + } + + $this->syslogWrapper($priority, $message); + } + + /** + * Formats a log record for the syslog output + * + * @param int $level The loglevel/priority + * @param string $message The message + * @param array $context The context of this call + * + * @return string the formatted syslog output + */ + private function formatLog($level, $message, $context = []) + { + $record = $this->introspection->getRecord(); + $record = array_merge($record, ['uid' => $this->logUid]); + $logMessage = ''; + + $logMessage .= $this->channel . ' '; + $logMessage .= '[' . $this->logToString[$level] . ']: '; + $logMessage .= $this->psrInterpolate($message, $context) . ' '; + $logMessage .= @json_encode($context) . ' - '; + $logMessage .= @json_encode($record); + + return $logMessage; + } + + private function customErrorHandler($code, $msg) + { + $this->errorMessage = preg_replace('{^(fopen|mkdir)\(.*?\): }', '', $msg); + } + + /** + * A syslog wrapper to make syslog functionality testable + * + * @param int $level The syslog priority + * @param string $entry The message to send to the syslog function + */ + protected function syslogWrapper($level, $entry) + { + set_error_handler([$this, 'customErrorHandler']); + $written = syslog($level, $entry); + restore_error_handler(); + + if (!$written) { + throw new \UnexpectedValueException(sprintf('Can\'t write into syslog for ident "%s" and facility "%s": ' . $this->errorMessage, $this->channel, $this->logFacility)); + } + } +} diff --git a/src/Util/Logger/VoidLogger.php b/src/Util/Logger/VoidLogger.php new file mode 100644 index 0000000000..16cd10da05 --- /dev/null +++ b/src/Util/Logger/VoidLogger.php @@ -0,0 +1,140 @@ +getBasePath()); + // Just in case the last update wasn't failed + if (Config::get('system', 'update', Update::SUCCESS, true) != Update::FAILED) { + Update::run(self::getApp()->getBasePath()); + } } } diff --git a/src/Worker/Notifier.php b/src/Worker/Notifier.php index baae33f7e0..dac3346f95 100644 --- a/src/Worker/Notifier.php +++ b/src/Worker/Notifier.php @@ -550,7 +550,7 @@ class Notifier */ private static function isRemovalActivity($cmd, $owner, $network) { - return ($cmd == Delivery::DELETION) && $owner['account_removed'] && in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DIASPORA]); + return ($cmd == Delivery::DELETION) && $owner['account_removed'] && in_array($network, [Protocol::ACTIVITYPUB, Protocol::DIASPORA]); } /** diff --git a/src/Worker/README.md b/src/Worker/README.md new file mode 100644 index 0000000000..fc937b33bc --- /dev/null +++ b/src/Worker/README.md @@ -0,0 +1,6 @@ +## Friendica\Worker + +The Worker namespace contains all asynchronous workers of Friendica. +The all have to implement the function `public static function execute()`. + +They are all executed by the [`Worker`](https://github.com/friendica/friendica/blob/develop/src/Core/Worker.php). \ No newline at end of file diff --git a/tests/DatabaseTest.php b/tests/DatabaseTest.php index 3ff4c6fe14..fec31b05af 100644 --- a/tests/DatabaseTest.php +++ b/tests/DatabaseTest.php @@ -5,10 +5,11 @@ namespace Friendica\Test; -use Friendica\Core\Config\Cache; +use Friendica\App; use Friendica\Database\DBA; use Friendica\Factory; use Friendica\Util\BasePath; +use Friendica\Util\Config\ConfigFileLoader; use Friendica\Util\Profiler; use PHPUnit\DbUnit\DataSet\YamlDataSet; use PHPUnit\DbUnit\TestCaseTrait; @@ -41,7 +42,8 @@ abstract class DatabaseTest extends MockedTest } $basePath = BasePath::create(dirname(__DIR__)); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $configLoader = new ConfigFileLoader($basePath, $mode); $config = Factory\ConfigFactory::createCache($configLoader); $profiler = \Mockery::mock(Profiler::class); diff --git a/tests/Util/AppMockTrait.php b/tests/Util/AppMockTrait.php index 817570dd58..23920ff6f4 100644 --- a/tests/Util/AppMockTrait.php +++ b/tests/Util/AppMockTrait.php @@ -30,19 +30,27 @@ trait AppMockTrait */ protected $profilerMock; + /** + * @var MockInterface|App\Mode The mocked App mode + */ + protected $mode; + /** * Mock the App * * @param vfsStreamDirectory $root The root directory + * @param Config\Cache\ConfigCache $configCache + * @param bool $raw If true, no config mocking will be done */ - public function mockApp($root) + public function mockApp(vfsStreamDirectory $root, $configCache = null, $raw = false) { $this->configMock = \Mockery::mock(Config\Cache\IConfigCache::class); + $this->mode = \Mockery::mock(App\Mode::class); $configAdapterMock = \Mockery::mock(Config\Adapter\IConfigAdapter::class); // Disable the adapter $configAdapterMock->shouldReceive('isConnected')->andReturn(false); - $config = new Config\Configuration($this->configMock, $configAdapterMock); + $config = new Config\Configuration((isset($configCache) ? $configCache : $this->configMock), $configAdapterMock); // Initialize empty Config Config::init($config); @@ -52,6 +60,37 @@ trait AppMockTrait ->shouldReceive('getBasePath') ->andReturn($root->url()); + $this->app + ->shouldReceive('getMode') + ->andReturn($this->mode); + + $this->profilerMock = \Mockery::mock(Profiler::class); + $this->profilerMock->shouldReceive('saveTimestamp'); + + $this->app + ->shouldReceive('getConfigCache') + ->andReturn((isset($configCache) ? $configCache : $this->configMock)); + $this->app + ->shouldReceive('getTemplateEngine') + ->andReturn(new FriendicaSmartyEngine()); + $this->app + ->shouldReceive('getCurrentTheme') + ->andReturn('Smarty3'); + $this->app + ->shouldReceive('getProfiler') + ->andReturn($this->profilerMock); + $this->app + ->shouldReceive('getBaseUrl') + ->andReturnUsing(function () { + return $this->app->getConfigCache()->get('system', 'url'); + }); + + BaseObject::setApp($this->app); + + if ($raw) { + return; + } + $this->configMock ->shouldReceive('has') ->andReturn(true); @@ -79,26 +118,5 @@ trait AppMockTrait ->shouldReceive('get') ->with('system', 'theme') ->andReturn('system_theme'); - - $this->profilerMock = \Mockery::mock(Profiler::class); - $this->profilerMock->shouldReceive('saveTimestamp'); - - $this->app - ->shouldReceive('getConfigCache') - ->andReturn($this->configMock); - $this->app - ->shouldReceive('getTemplateEngine') - ->andReturn(new FriendicaSmartyEngine()); - $this->app - ->shouldReceive('getCurrentTheme') - ->andReturn('Smarty3'); - $this->app - ->shouldReceive('getBaseUrl') - ->andReturn('http://friendica.local'); - $this->app - ->shouldReceive('getProfiler') - ->andReturn($this->profilerMock); - - BaseObject::setApp($this->app); } } diff --git a/tests/Util/RendererMockTrait.php b/tests/Util/RendererMockTrait.php index f4e05f39b8..b0fa8132c9 100644 --- a/tests/Util/RendererMockTrait.php +++ b/tests/Util/RendererMockTrait.php @@ -36,16 +36,20 @@ trait RendererMockTrait * Mocking the method 'Renderer::replaceMacros()' * * @param string $template The template to use (normally, it is the mock result of 'mockGetMarkupTemplate()' - * @param array $args The arguments to pass to the macro + * @param array|\Closure|null $args The arguments to pass to the macro * @param string $return the return value of the mock * @param null|int $times How often the method will get used */ - public function mockReplaceMacros($template, $args = [], $return = '', $times = null) + public function mockReplaceMacros($template, $args = null, $return = '', $times = null) { if (!isset($this->rendererMock)) { $this->rendererMock = \Mockery::mock('alias:' . Renderer::class); } + if (!isset($args)) { + $args = []; + } + $this->rendererMock ->shouldReceive('replaceMacros') ->with($template, $args) diff --git a/tests/Util/VFSTrait.php b/tests/Util/VFSTrait.php index 320d989cce..e9b7dfc509 100644 --- a/tests/Util/VFSTrait.php +++ b/tests/Util/VFSTrait.php @@ -25,7 +25,7 @@ trait VFSTrait ]; // create a virtual directory and copy all needed files and folders to it - $this->root = vfsStream::setup('friendica', null, $structure); + $this->root = vfsStream::setup('friendica', 0777, $structure); $this->setConfigFile('defaults.config.php'); $this->setConfigFile('settings.config.php'); diff --git a/tests/datasets/config/.htconfig.test.php b/tests/datasets/config/.htconfig.php similarity index 95% rename from tests/datasets/config/.htconfig.test.php rename to tests/datasets/config/.htconfig.php index 193142c49c..04e3e9cc96 100644 --- a/tests/datasets/config/.htconfig.test.php +++ b/tests/datasets/config/.htconfig.php @@ -33,7 +33,7 @@ $a->config['sitename'] = "Friendica My Network"; // must precisely match the email address of the person logged in. $a->config['register_policy'] = REGISTER_OPEN; $a->config['register_text'] = 'A register text'; -$a->config['admin_email'] = 'admin@friendica.local'; +$a->config['admin_email'] = 'admin@test.it'; $a->config['admin_nickname'] = 'Friendly admin'; // Maximum size of an imported message, 0 is unlimited @@ -52,7 +52,7 @@ $a->config['system']['huburl'] = '[internal]'; $a->config['system']['allowed_themes'] = 'quattro,vier,duepuntozero'; // default system theme -$a->config['system']['theme'] = 'duepuntozero'; +$a->config['system']['theme'] = 'frio'; // By default allow pseudonyms $a->config['system']['no_regfullname'] = true; diff --git a/tests/datasets/config/local.config.php b/tests/datasets/config/local.config.php index 8a392909f2..f28e1f2e85 100644 --- a/tests/datasets/config/local.config.php +++ b/tests/datasets/config/local.config.php @@ -23,5 +23,6 @@ return [ 'system' => [ 'default_timezone' => 'UTC', 'language' => 'en', + 'theme' => 'frio', ], ]; diff --git a/tests/datasets/config/local.ini.php b/tests/datasets/config/local.ini.php index 1fea0b028e..a9e462d13e 100644 --- a/tests/datasets/config/local.ini.php +++ b/tests/datasets/config/local.ini.php @@ -11,6 +11,9 @@ username = testuser password = testpw database = testdb +[system] +theme = frio + [config] admin_email = admin@test.it INI; diff --git a/tests/include/ApiTest.php b/tests/include/ApiTest.php index 2f8becc18c..80a25c20c1 100644 --- a/tests/include/ApiTest.php +++ b/tests/include/ApiTest.php @@ -7,13 +7,13 @@ namespace Friendica\Test; use Friendica\App; use Friendica\Core\Config; -use Friendica\Core\Config\Cache; use Friendica\Core\PConfig; use Friendica\Core\Protocol; use Friendica\Core\System; use Friendica\Factory; use Friendica\Network\HTTPException; use Friendica\Util\BasePath; +use Friendica\Util\Config\ConfigFileLoader; use Monolog\Handler\TestHandler; require_once __DIR__ . '/../../include/api.php'; @@ -31,20 +31,33 @@ class ApiTest extends DatabaseTest */ protected $logOutput; + /** @var App */ + protected $app; + + /** @var array */ + protected $selfUser; + /** @var array */ + protected $friendUser; + /** @var array */ + protected $otherUser; + + protected $wrongUserId; + /** * Create variables used by tests. */ public function setUp() { $basePath = BasePath::create(dirname(__DIR__) . '/../'); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $configLoader = new ConfigFileLoader($basePath, $mode); $configCache = Factory\ConfigFactory::createCache($configLoader); $profiler = Factory\ProfilerFactory::create($configCache); Factory\DBFactory::init($basePath, $configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); - $this->app = new App($basePath, $config, $logger, $profiler, false); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); + $this->app = new App($config, $mode, $logger, $profiler, false); parent::setUp(); @@ -1271,31 +1284,30 @@ class ApiTest extends DatabaseTest /** * Test the api_status_show() function. - * @return void */ - public function testApiStatusShow() + public function testApiStatusShowWithJson() { - $result = api_status_show('json'); + $result = api_status_show('json', 1); $this->assertStatus($result['status']); } /** * Test the api_status_show() function with an XML result. - * @return void */ public function testApiStatusShowWithXml() { - $result = api_status_show('xml'); + $result = api_status_show('xml', 1); $this->assertXml($result, 'statuses'); } /** - * Test the api_status_show() function with a raw result. - * @return void + * Test the api_get_last_status() function */ - public function testApiStatusShowWithRaw() + public function testApiGetLastStatus() { - $this->assertStatus(api_status_show('raw')); + $item = api_get_last_status($this->selfUser['id'], $this->selfUser['id']); + + $this->assertNotNull($item); } /** diff --git a/tests/src/Content/SmiliesTest.php b/tests/src/Content/SmiliesTest.php new file mode 100644 index 0000000000..40d126e005 --- /dev/null +++ b/tests/src/Content/SmiliesTest.php @@ -0,0 +1,69 @@ +setUpVfsDir(); + $this->mockApp($this->root); + $this->app->videowidth = 425; + $this->app->videoheight = 350; + $this->configMock->shouldReceive('get') + ->with('system', 'no_smilies') + ->andReturn(false); + $this->configMock->shouldReceive('get') + ->with(false, 'system', 'no_smilies') + ->andReturn(false); + } + + public function dataLinks() + { + return [ + /** @see https://github.com/friendica/friendica/pull/6933 */ + 'bug-6933-1' => [ + 'data' => '/', + 'smilies' => ['texts' => [], 'icons' => []], + 'expected' => '/', + ], + 'bug-6933-2' => [ + 'data' => 'code', + 'smilies' => ['texts' => [], 'icons' => []], + 'expected' => 'code', + ], + ]; + } + + /** + * Test replace smilies in different texts + * @dataProvider dataLinks + * + * @param string $text Test string + * @param array $smilies List of smilies to replace + * @param string $expected Expected result + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function testReplaceFromArray($text, $smilies, $expected) + { + $output = Smilies::replaceFromArray($text, $smilies); + $this->assertEquals($expected, $output); + } +} diff --git a/tests/src/Content/Text/BBCodeTest.php b/tests/src/Content/Text/BBCodeTest.php index 7fd39927fc..802a90278b 100644 --- a/tests/src/Content/Text/BBCodeTest.php +++ b/tests/src/Content/Text/BBCodeTest.php @@ -1,130 +1,133 @@ -setUpVfsDir(); - $this->mockApp($this->root); - $this->app->videowidth = 425; - $this->app->videoheight = 350; - $this->configMock->shouldReceive('get') - ->with('system', 'remove_multiplicated_lines') - ->andReturn(false); - $this->configMock->shouldReceive('get') - ->with('system', 'no_oembed') - ->andReturn(false); - $this->configMock->shouldReceive('get') - ->with('system', 'allowed_link_protocols') - ->andReturn(null); - $this->configMock->shouldReceive('get') - ->with('system', 'itemcache_duration') - ->andReturn(-1); - $this->mockL10nT(); - } - - public function dataLinks() - { - return [ - /** @see https://github.com/friendica/friendica/issues/2487 */ - 'bug-2487-1' => [ - 'data' => 'https://de.wikipedia.org/wiki/Juha_Sipilä', - 'assertHTML' => true, - ], - 'bug-2487-2' => [ - 'data' => 'https://de.wikipedia.org/wiki/Dnepr_(Motorradmarke)', - 'assertHTML' => true, - ], - 'bug-2487-3' => [ - 'data' => 'https://friendica.wäckerlin.ch/friendica', - 'assertHTML' => true, - ], - 'bug-2487-4' => [ - 'data' => 'https://mastodon.social/@morevnaproject', - 'assertHTML' => true, - ], - /** @see https://github.com/friendica/friendica/issues/5795 */ - 'bug-5795' => [ - 'data' => 'https://social.nasqueron.org/@liw/100798039015010628', - 'assertHTML' => true, - ], - /** @see https://github.com/friendica/friendica/issues/6095 */ - 'bug-6095' => [ - 'data' => 'https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)', - 'assertHTML' => true, - ], - 'no-protocol' => [ - 'data' => 'example.com/path', - 'assertHTML' => false - ], - 'wrong-protocol' => [ - 'data' => 'ftp://example.com', - 'assertHTML' => false - ], - 'wrong-domain-without-path' => [ - 'data' => 'http://example', - 'assertHTML' => false - ], - 'wrong-domain-with-path' => [ - 'data' => 'http://example/path', - 'assertHTML' => false - ], - 'bug-6857-domain-start' => [ - 'data' => "http://\nexample.com", - 'assertHTML' => false - ], - 'bug-6857-domain-end' => [ - 'data' => "http://example\n.com", - 'assertHTML' => false - ], - 'bug-6857-tld' => [ - 'data' => "http://example.\ncom", - 'assertHTML' => false - ], - 'bug-6857-end' => [ - 'data' => "http://example.com\ntest", - 'assertHTML' => false - ], - 'bug-6901' => [ - 'data' => "http://example.com
    ", - 'assertHTML' => false - ], - ]; - } - - /** - * Test convert different links inside a text - * @dataProvider dataLinks - * - * @param string $data The data to text - * @param bool $assertHTML True, if the link is a HTML link (...) - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public function testAutoLinking($data, $assertHTML) - { - $output = BBCode::convert($data); - $assert = '' . $data . ''; - if ($assertHTML) { - $this->assertEquals($assert, $output); - } else { - $this->assertNotEquals($assert, $output); - } - } -} +setUpVfsDir(); + $this->mockApp($this->root); + $this->app->videowidth = 425; + $this->app->videoheight = 350; + $this->configMock->shouldReceive('get') + ->with('system', 'remove_multiplicated_lines') + ->andReturn(false); + $this->configMock->shouldReceive('get') + ->with('system', 'no_oembed') + ->andReturn(false); + $this->configMock->shouldReceive('get') + ->with('system', 'allowed_link_protocols') + ->andReturn(null); + $this->configMock->shouldReceive('get') + ->with('system', 'itemcache_duration') + ->andReturn(-1); + $this->configMock->shouldReceive('get') + ->with('system', 'url') + ->andReturn('friendica.local'); + $this->mockL10nT(); + } + + public function dataLinks() + { + return [ + /** @see https://github.com/friendica/friendica/issues/2487 */ + 'bug-2487-1' => [ + 'data' => 'https://de.wikipedia.org/wiki/Juha_Sipilä', + 'assertHTML' => true, + ], + 'bug-2487-2' => [ + 'data' => 'https://de.wikipedia.org/wiki/Dnepr_(Motorradmarke)', + 'assertHTML' => true, + ], + 'bug-2487-3' => [ + 'data' => 'https://friendica.wäckerlin.ch/friendica', + 'assertHTML' => true, + ], + 'bug-2487-4' => [ + 'data' => 'https://mastodon.social/@morevnaproject', + 'assertHTML' => true, + ], + /** @see https://github.com/friendica/friendica/issues/5795 */ + 'bug-5795' => [ + 'data' => 'https://social.nasqueron.org/@liw/100798039015010628', + 'assertHTML' => true, + ], + /** @see https://github.com/friendica/friendica/issues/6095 */ + 'bug-6095' => [ + 'data' => 'https://en.wikipedia.org/wiki/Solid_(web_decentralization_project)', + 'assertHTML' => true, + ], + 'no-protocol' => [ + 'data' => 'example.com/path', + 'assertHTML' => false + ], + 'wrong-protocol' => [ + 'data' => 'ftp://example.com', + 'assertHTML' => false + ], + 'wrong-domain-without-path' => [ + 'data' => 'http://example', + 'assertHTML' => false + ], + 'wrong-domain-with-path' => [ + 'data' => 'http://example/path', + 'assertHTML' => false + ], + 'bug-6857-domain-start' => [ + 'data' => "http://\nexample.com", + 'assertHTML' => false + ], + 'bug-6857-domain-end' => [ + 'data' => "http://example\n.com", + 'assertHTML' => false + ], + 'bug-6857-tld' => [ + 'data' => "http://example.\ncom", + 'assertHTML' => false + ], + 'bug-6857-end' => [ + 'data' => "http://example.com\ntest", + 'assertHTML' => false + ], + 'bug-6901' => [ + 'data' => "http://example.com
      ", + 'assertHTML' => false + ], + ]; + } + + /** + * Test convert different links inside a text + * @dataProvider dataLinks + * + * @param string $data The data to text + * @param bool $assertHTML True, if the link is a HTML link (...) + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public function testAutoLinking($data, $assertHTML) + { + $output = BBCode::convert($data); + $assert = '' . $data . ''; + if ($assertHTML) { + $this->assertEquals($assert, $output); + } else { + $this->assertNotEquals($assert, $output); + } + } +} diff --git a/tests/src/Core/Cache/DatabaseCacheDriverTest.php b/tests/src/Core/Cache/DatabaseCacheDriverTest.php index 775a083a9b..2d29c2ad96 100644 --- a/tests/src/Core/Cache/DatabaseCacheDriverTest.php +++ b/tests/src/Core/Cache/DatabaseCacheDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Cache; use Friendica\Core\Cache; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Test\Util\DbaCacheMockTrait; /** diff --git a/tests/src/Core/Cache/MemcacheCacheDriverTest.php b/tests/src/Core/Cache/MemcacheCacheDriverTest.php index f9df9eaba0..f8de88ac95 100644 --- a/tests/src/Core/Cache/MemcacheCacheDriverTest.php +++ b/tests/src/Core/Cache/MemcacheCacheDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Cache; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; /** * @requires extension memcache diff --git a/tests/src/Core/Cache/MemcachedCacheDriverTest.php b/tests/src/Core/Cache/MemcachedCacheDriverTest.php index 4e16ef947f..9f0ed8d4fe 100644 --- a/tests/src/Core/Cache/MemcachedCacheDriverTest.php +++ b/tests/src/Core/Cache/MemcachedCacheDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Cache; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; /** * @requires extension memcached diff --git a/tests/src/Core/Cache/RedisCacheDriverTest.php b/tests/src/Core/Cache/RedisCacheDriverTest.php index 20fe7eb53f..80baa9f4b3 100644 --- a/tests/src/Core/Cache/RedisCacheDriverTest.php +++ b/tests/src/Core/Cache/RedisCacheDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Cache; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; /** * @requires extension redis diff --git a/tests/src/Core/Console/AutomaticInstallationConsoleTest.php b/tests/src/Core/Console/AutomaticInstallationConsoleTest.php index 73b6835fb5..f24f56cdb5 100644 --- a/tests/src/Core/Console/AutomaticInstallationConsoleTest.php +++ b/tests/src/Core/Console/AutomaticInstallationConsoleTest.php @@ -2,11 +2,15 @@ namespace Friendica\Test\src\Core\Console; +use Friendica\Core\Config\Cache\ConfigCache; use Friendica\Core\Console\AutomaticInstallation; +use Friendica\Core\Installer; +use Friendica\Core\Logger; use Friendica\Test\Util\DBAMockTrait; use Friendica\Test\Util\DBStructureMockTrait; use Friendica\Test\Util\L10nMockTrait; use Friendica\Test\Util\RendererMockTrait; +use Friendica\Util\Logger\VoidLogger; use org\bovigo\vfs\vfsStream; use org\bovigo\vfs\vfsStreamFile; @@ -22,12 +26,6 @@ class AutomaticInstallationConsoleTest extends ConsoleTest use DBStructureMockTrait; use RendererMockTrait; - private $db_host; - private $db_port; - private $db_data; - private $db_user; - private $db_pass; - /** * @var vfsStreamFile Assert file without DB credentials */ @@ -37,6 +35,11 @@ class AutomaticInstallationConsoleTest extends ConsoleTest */ private $assertFileDb; + /** + * @var ConfigCache The configuration cache to check after each test + */ + private $configCache; + public function setUp() { parent::setUp(); @@ -46,42 +49,104 @@ class AutomaticInstallationConsoleTest extends ConsoleTest ->removeChild('local.config.php'); } - $this->db_host = getenv('MYSQL_HOST'); - $this->db_port = !empty(getenv('MYSQL_PORT')) ? getenv('MYSQL_PORT') : null; - $this->db_data = getenv('MYSQL_DATABASE'); - $this->db_user = getenv('MYSQL_USERNAME') . getenv('MYSQL_USER'); - $this->db_pass = getenv('MYSQL_PASSWORD'); - - $this->configMock - ->shouldReceive('get') - ->with('config', 'php_path') - ->andReturn(false); - $this->mockL10nT(); + + $this->configCache = new ConfigCache(); + $this->configCache->set('system', 'basepath', $this->root->url()); + $this->configCache->set('config', 'php_path', trim(shell_exec('which php'))); + $this->configCache->set('system', 'theme', 'smarty3'); + + $this->mockApp($this->root, null, true); + + $this->configMock->shouldReceive('set')->andReturnUsing(function ($cat, $key, $value) { + if ($key !== 'basepath') { + return $this->configCache->set($cat, $key, $value); + } else { + return true; + } + }); + + $this->configMock->shouldReceive('has')->andReturn(true); + $this->configMock->shouldReceive('get')->andReturnUsing(function ($cat, $key) { + return $this->configCache->get($cat, $key); + }); + $this->configMock->shouldReceive('load')->andReturnUsing(function ($config, $overwrite = false) { + return $this->configCache->load($config, $overwrite); + }); + + $this->mode->shouldReceive('isInstall')->andReturn(true); + Logger::init(new VoidLogger()); } /** - * Creates the arguments which is asserted to be passed to 'replaceMacros()' for creating the local.config.php + * Returns the dataset for each automatic installation test * - * @param bool $withDb if true, DB will get saved too - * - * @return array The arguments to pass to the mock for 'replaceMacros()' + * @return array the dataset */ - private function createArgumentsForMacro($withDb) + public function dataInstaller() { - $args = [ - '$phpath' => trim(shell_exec('which php')), - '$dbhost' => (($withDb) ? $this->db_host . (isset($this->db_port) ? ':' . $this->db_port : '') : ''), - '$dbuser' => (($withDb) ? $this->db_user : ''), - '$dbpass' => (($withDb) ? $this->db_pass : ''), - '$dbdata' => (($withDb) ? $this->db_data : ''), - '$timezone' => 'Europe/Berlin', - '$language' => 'de', - '$urlpath' => '/friendica', - '$adminmail' => 'admin@friendica.local' + return [ + 'empty' => [ + 'data' => [ + 'database' => [ + 'hostname' => '', + 'username' => '', + 'password' => '', + 'database' => '', + 'port' => '', + ], + 'config' => [ + 'php_path' => '', + 'admin_email' => '', + ], + 'system' => [ + 'urlpath' => '', + 'default_timezone' => '', + 'language' => '', + ], + ], + ], + 'normal' => [ + 'data' => [ + 'database' => [ + 'hostname' => 'testhost', + 'port' => 3306, + 'username' => 'friendica', + 'password' => 'a password', + 'database' => 'database', + ], + 'config' => [ + 'php_path' => '', + 'admin_email' => 'admin@philipp.info', + ], + 'system' => [ + 'urlpath' => 'test/it', + 'default_timezone' => 'en', + 'language' => 'Europe/Berlin', + ], + ], + ], + 'special' => [ + 'data' => [ + 'database' => [ + 'hostname' => 'testhost.new.domain', + 'port' => 3341, + 'username' => 'fr"§%ica', + 'password' => '$%\"gse', + 'database' => 'db', + ], + 'config' => [ + 'php_path' => '', + 'admin_email' => 'admin@philipp.info', + ], + 'system' => [ + 'urlpath' => 'test/it', + 'default_timezone' => 'en', + 'language' => 'Europe/Berlin', + ], + ], + ], ]; - - return $args; } private function assertFinished($txt, $withconfig = false, $copyfile = false) @@ -177,15 +242,95 @@ FIN; } /** - * @medium + * Asserts one config entry + * + * @param string $cat The category to test + * @param string $key The key to test + * @param null|array $assertion The asserted value (null = empty, or array/string) + * @param string $default_value The default value */ - public function testWithConfig() + public function assertConfigEntry($cat, $key, $assertion = null, $default_value = null) + { + if (!empty($assertion[$cat][$key])) { + $this->assertEquals($assertion[$cat][$key], $this->configCache->get($cat, $key)); + } elseif (!empty($assertion) && !is_array($assertion)) { + $this->assertEquals($assertion, $this->configCache->get($cat, $key)); + } elseif (!empty($default_value)) { + $this->assertEquals($default_value, $this->configCache->get($cat, $key)); + } else { + $this->assertEmpty($this->configCache->get($cat, $key), $this->configCache->get($cat, $key)); + } + } + + /** + * Asserts all config entries + * + * @param null|array $assertion The optional assertion array + * @param boolean $saveDb True, if the db credentials should get saved to the file + * @param boolean $default True, if we use the default values + * @param boolean $defaultDb True, if we use the default value for the DB + */ + public function assertConfig($assertion = null, $saveDb = false, $default = true, $defaultDb = true) + { + if (!empty($assertion['database']['hostname'])) { + $assertion['database']['hostname'] .= (!empty($assertion['database']['port']) ? ':' . $assertion['database']['port'] : ''); + } + + $this->assertConfigEntry('database', 'hostname', ($saveDb) ? $assertion : null, (!$saveDb || $defaultDb) ? Installer::DEFAULT_HOST : null); + $this->assertConfigEntry('database', 'username', ($saveDb) ? $assertion : null); + $this->assertConfigEntry('database', 'password', ($saveDb) ? $assertion : null); + $this->assertConfigEntry('database', 'database', ($saveDb) ? $assertion : null); + + $this->assertConfigEntry('config', 'admin_email', $assertion); + $this->assertConfigEntry('config', 'php_path', trim(shell_exec('which php'))); + + $this->assertConfigEntry('system', 'default_timezone', $assertion, ($default) ? Installer::DEFAULT_TZ : null); + $this->assertConfigEntry('system', 'language', $assertion, ($default) ? Installer::DEFAULT_LANG : null); + } + + /** + * Test the automatic installation without any parameter/setting + */ + public function testEmpty() + { + $this->app->shouldReceive('getURLPath')->andReturn('')->atLeast()->once(); + + $this->mockConnect(true, 1); + $this->mockConnected(true, 1); + $this->mockExistsTable('user', false, 1); + $this->mockUpdate([$this->root->url(), false, true, true], null, 1); + + $this->mockGetMarkupTemplate('local.config.tpl', 'testTemplate', 1); + $this->mockReplaceMacros('testTemplate', \Mockery::any(), '', 1); + + $console = new AutomaticInstallation($this->consoleArgv); + + $txt = $this->dumpExecute($console); + + $this->assertFinished($txt, true, false); + $this->assertTrue($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')); + + $this->assertConfig(); + } + + /** + * Test the automatic installation with a prepared config file + * @dataProvider dataInstaller + */ + public function testWithConfig(array $data) { $this->mockConnect(true, 1); $this->mockConnected(true, 1); $this->mockExistsTable('user', false, 1); $this->mockUpdate([$this->root->url(), false, true, true], null, 1); + $conf = function ($cat, $key) use ($data) { + if ($cat == 'database' && $key == 'hostname' && !empty($data['database']['port'])) { + return $data[$cat][$key] . ':' . $data['database']['port']; + } + return $data[$cat][$key]; + }; + $config = << [ - 'hostname' => '', - 'username' => '', - 'password' => '', - 'database' => '', + 'hostname' => '{$conf('database', 'hostname')}', + 'username' => '{$conf('database', 'username')}', + 'password' => '{$conf('database', 'password')}', + 'database' => '{$conf('database', 'database')}', 'charset' => 'utf8mb4', ], @@ -210,14 +355,15 @@ return [ // **************************************************************** 'config' => [ - 'admin_email' => '', + 'admin_email' => '{$conf('config', 'admin_email')}', 'sitename' => 'Friendica Social Network', 'register_policy' => \Friendica\Module\Register::OPEN, 'register_text' => '', ], 'system' => [ - 'default_timezone' => 'UTC', - 'language' => 'en', + 'urlpath' => '{$conf('system', 'urlpath')}', + 'default_timezone' => '{$conf('system', 'default_timezone')}', + 'language' => '{$conf('system', 'language')}', ], ]; CONF; @@ -234,25 +380,39 @@ CONF; $this->assertFinished($txt, false, true); $this->assertTrue($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')); + $this->assertEquals($config, file_get_contents($this->root->getChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')->url())); + + $this->assertConfig($data, true, false, false); } /** - * @medium + * Test the automatic installation with environment variables + * Includes saving the DB credentials to the file + * @dataProvider dataInstaller */ - public function testWithEnvironmentAndSave() + public function testWithEnvironmentAndSave(array $data) { + $this->app->shouldReceive('getURLPath')->andReturn('')->atLeast()->once(); + $this->mockConnect(true, 1); $this->mockConnected(true, 1); $this->mockExistsTable('user', false, 1); $this->mockUpdate([$this->root->url(), false, true, true], null, 1); $this->mockGetMarkupTemplate('local.config.tpl', 'testTemplate', 1); - $this->mockReplaceMacros('testTemplate', $this->createArgumentsForMacro(true), '', 1); + $this->mockReplaceMacros('testTemplate', \Mockery::any(), '', 1); - $this->assertTrue(putenv('FRIENDICA_ADMIN_MAIL=admin@friendica.local')); - $this->assertTrue(putenv('FRIENDICA_TZ=Europe/Berlin')); - $this->assertTrue(putenv('FRIENDICA_LANG=de')); - $this->assertTrue(putenv('FRIENDICA_URL_PATH=/friendica')); + $this->assertTrue(putenv('MYSQL_HOST=' . $data['database']['hostname'])); + $this->assertTrue(putenv('MYSQL_PORT=' . $data['database']['port'])); + $this->assertTrue(putenv('MYSQL_DATABASE=' . $data['database']['database'])); + $this->assertTrue(putenv('MYSQL_USERNAME=' . $data['database']['username'])); + $this->assertTrue(putenv('MYSQL_PASSWORD=' . $data['database']['password'])); + + $this->assertTrue(putenv('FRIENDICA_URL_PATH=' . $data['system']['urlpath'])); + $this->assertTrue(putenv('FRIENDICA_PHP_PATH=' . $data['config']['php_path'])); + $this->assertTrue(putenv('FRIENDICA_ADMIN_MAIL=' . $data['config']['admin_email'])); + $this->assertTrue(putenv('FRIENDICA_TZ=' . $data['system']['default_timezone'])); + $this->assertTrue(putenv('FRIENDICA_LANG=' . $data['system']['language'])); $console = new AutomaticInstallation($this->consoleArgv); $console->setOption('savedb', true); @@ -260,90 +420,105 @@ CONF; $txt = $this->dumpExecute($console); $this->assertFinished($txt, true); + $this->assertConfig($data, true, true, false); } /** - * @medium + * Test the automatic installation with environment variables + * Don't save the db credentials to the file + * @dataProvider dataInstaller */ - public function testWithEnvironmentWithoutSave() + public function testWithEnvironmentWithoutSave(array $data) { + $this->app->shouldReceive('getURLPath')->andReturn('')->atLeast()->once(); + $this->mockConnect(true, 1); $this->mockConnected(true, 1); $this->mockExistsTable('user', false, 1); $this->mockUpdate([$this->root->url(), false, true, true], null, 1); $this->mockGetMarkupTemplate('local.config.tpl', 'testTemplate', 1); - $this->mockReplaceMacros('testTemplate', $this->createArgumentsForMacro(false), '', 1); + $this->mockReplaceMacros('testTemplate', \Mockery::any(), '', 1); - $this->assertTrue(putenv('FRIENDICA_ADMIN_MAIL=admin@friendica.local')); - $this->assertTrue(putenv('FRIENDICA_TZ=Europe/Berlin')); - $this->assertTrue(putenv('FRIENDICA_LANG=de')); - $this->assertTrue(putenv('FRIENDICA_URL_PATH=/friendica')); + $this->assertTrue(putenv('MYSQL_HOST=' . $data['database']['hostname'])); + $this->assertTrue(putenv('MYSQL_PORT=' . $data['database']['port'])); + $this->assertTrue(putenv('MYSQL_DATABASE=' . $data['database']['database'])); + $this->assertTrue(putenv('MYSQL_USERNAME=' . $data['database']['username'])); + $this->assertTrue(putenv('MYSQL_PASSWORD=' . $data['database']['password'])); + + $this->assertTrue(putenv('FRIENDICA_URL_PATH=' . $data['system']['urlpath'])); + $this->assertTrue(putenv('FRIENDICA_PHP_PATH=' . $data['config']['php_path'])); + $this->assertTrue(putenv('FRIENDICA_ADMIN_MAIL=' . $data['config']['admin_email'])); + $this->assertTrue(putenv('FRIENDICA_TZ=' . $data['system']['default_timezone'])); + $this->assertTrue(putenv('FRIENDICA_LANG=' . $data['system']['language'])); $console = new AutomaticInstallation($this->consoleArgv); $txt = $this->dumpExecute($console); $this->assertFinished($txt, true); + $this->assertConfig($data, false, true); } /** - * @medium + * Test the automatic installation with arguments + * @dataProvider dataInstaller */ - public function testWithArguments() + public function testWithArguments(array $data) { + $this->app->shouldReceive('getURLPath')->andReturn('')->atLeast()->once(); + $this->mockConnect(true, 1); $this->mockConnected(true, 1); $this->mockExistsTable('user', false, 1); $this->mockUpdate([$this->root->url(), false, true, true], null, 1); $this->mockGetMarkupTemplate('local.config.tpl', 'testTemplate', 1); - $this->mockReplaceMacros('testTemplate', $this->createArgumentsForMacro(true), '', 1); + $this->mockReplaceMacros('testTemplate', \Mockery::any(), '', 1); $console = new AutomaticInstallation($this->consoleArgv); - $console->setOption('dbhost', $this->db_host); - $console->setOption('dbuser', $this->db_user); - if (!empty($this->db_pass)) { - $console->setOption('dbpass', $this->db_pass); - } - if (!empty($this->db_port)) { - $console->setOption('dbport', $this->db_port); - } - $console->setOption('dbdata', $this->db_data); - - $console->setOption('admin', 'admin@friendica.local'); - $console->setOption('tz', 'Europe/Berlin'); - $console->setOption('lang', 'de'); - - $console->setOption('urlpath', '/friendica'); + $option = function($var, $cat, $key) use ($data, $console) { + if (!empty($data[$cat][$key])) { + $console->setOption($var, $data[$cat][$key]); + } + }; + $option('dbhost' , 'database', 'hostname'); + $option('dbport' , 'database', 'port'); + $option('dbuser' , 'database', 'username'); + $option('dbpass' , 'database', 'password'); + $option('dbdata' , 'database', 'database'); + $option('urlpath' , 'system' , 'urlpath'); + $option('phppath' , 'config' , 'php_path'); + $option('admin' , 'config' , 'admin_email'); + $option('tz' , 'system' , 'default_timezone'); + $option('lang' , 'system' , 'language'); $txt = $this->dumpExecute($console); $this->assertFinished($txt, true); + $this->assertConfig($data, true, true, true); } /** - * @runTestsInSeparateProcesses - * @preserveGlobalState disabled + * Test the automatic installation with a wrong database connection */ public function testNoDatabaseConnection() { + $this->app->shouldReceive('getURLPath')->andReturn('')->atLeast()->once(); $this->mockConnect(false, 1); $this->mockGetMarkupTemplate('local.config.tpl', 'testTemplate', 1); - $this->mockReplaceMacros('testTemplate', $this->createArgumentsForMacro(false), '', 1); - - $this->assertTrue(putenv('FRIENDICA_ADMIN_MAIL=admin@friendica.local')); - $this->assertTrue(putenv('FRIENDICA_TZ=Europe/Berlin')); - $this->assertTrue(putenv('FRIENDICA_LANG=de')); - $this->assertTrue(putenv('FRIENDICA_URL_PATH=/friendica')); + $this->mockReplaceMacros('testTemplate', \Mockery::any(), '', 1); $console = new AutomaticInstallation($this->consoleArgv); $txt = $this->dumpExecute($console); $this->assertStuckDB($txt); + $this->assertTrue($this->root->hasChild('config' . DIRECTORY_SEPARATOR . 'local.config.php')); + + $this->assertConfig(null, false, true, false); } public function testGetHelp() @@ -406,6 +581,6 @@ HELP; $txt = $this->dumpExecute($console); - $this->assertEquals($txt, $theHelp); + $this->assertEquals($theHelp, $txt); } } diff --git a/tests/src/Core/Console/ConfigConsoleTest.php b/tests/src/Core/Console/ConfigConsoleTest.php index ef50c19b72..c58b05ec39 100644 --- a/tests/src/Core/Console/ConfigConsoleTest.php +++ b/tests/src/Core/Console/ConfigConsoleTest.php @@ -16,20 +16,18 @@ class ConfigConsoleTest extends ConsoleTest { parent::setUp(); + $this->mockApp($this->root); + \Mockery::getConfiguration()->setConstantsMap([ Mode::class => [ 'DBCONFIGAVAILABLE' => 0 ] ]); - $mode = \Mockery::mock(Mode::class); - $mode + $this->mode ->shouldReceive('has') ->andReturn(true); - $this->app - ->shouldReceive('getMode') - ->andReturn($mode); } function testSetGetKeyValue() { diff --git a/tests/src/Core/Console/ConsoleTest.php b/tests/src/Core/Console/ConsoleTest.php index 4f7acc9c42..e142e60066 100644 --- a/tests/src/Core/Console/ConsoleTest.php +++ b/tests/src/Core/Console/ConsoleTest.php @@ -22,14 +22,9 @@ abstract class ConsoleTest extends MockedTest { parent::setUp(); - if (!getenv('MYSQL_DATABASE')) { - $this->markTestSkipped('Please set the MYSQL_* environment variables to your test database credentials.'); - } - Intercept::setUp(); $this->setUpVfsDir(); - $this->mockApp($this->root); } /** diff --git a/tests/src/Core/Lock/MemcacheCacheLockDriverTest.php b/tests/src/Core/Lock/MemcacheCacheLockDriverTest.php index ad20f5bfdb..8d32ad527d 100644 --- a/tests/src/Core/Lock/MemcacheCacheLockDriverTest.php +++ b/tests/src/Core/Lock/MemcacheCacheLockDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Lock; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Core\Lock\CacheLockDriver; /** diff --git a/tests/src/Core/Lock/MemcachedCacheLockDriverTest.php b/tests/src/Core/Lock/MemcachedCacheLockDriverTest.php index a5bdeaedb8..f08ffa3817 100644 --- a/tests/src/Core/Lock/MemcachedCacheLockDriverTest.php +++ b/tests/src/Core/Lock/MemcachedCacheLockDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Lock; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Core\Lock\CacheLockDriver; /** diff --git a/tests/src/Core/Lock/RedisCacheLockDriverTest.php b/tests/src/Core/Lock/RedisCacheLockDriverTest.php index 5f047bc664..d21f3b6d86 100644 --- a/tests/src/Core/Lock/RedisCacheLockDriverTest.php +++ b/tests/src/Core/Lock/RedisCacheLockDriverTest.php @@ -3,7 +3,7 @@ namespace Friendica\Test\src\Core\Lock; -use Friendica\Core\Cache\CacheDriverFactory; +use Friendica\Factory\CacheDriverFactory; use Friendica\Core\Lock\CacheLockDriver; /** diff --git a/tests/src/Database/DBATest.php b/tests/src/Database/DBATest.php index 511821da57..c941377219 100644 --- a/tests/src/Database/DBATest.php +++ b/tests/src/Database/DBATest.php @@ -3,25 +3,26 @@ namespace Friendica\Test\src\Database; use Friendica\App; use Friendica\Core\Config; -use Friendica\Core\Config\Cache; use Friendica\Database\DBA; use Friendica\Factory; use Friendica\Test\DatabaseTest; use Friendica\Util\BasePath; +use Friendica\Util\Config\ConfigFileLoader; class DBATest extends DatabaseTest { public function setUp() { $basePath = BasePath::create(dirname(__DIR__) . '/../../'); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $configLoader = new ConfigFileLoader($basePath, $mode); $configCache = Factory\ConfigFactory::createCache($configLoader); $profiler = Factory\ProfilerFactory::create($configCache); Factory\DBFactory::init($basePath, $configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); - $this->app = new App($basePath, $config, $logger, $profiler, false); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); + $this->app = new App($config, $mode, $logger, $profiler, false); parent::setUp(); diff --git a/tests/src/Database/DBStructureTest.php b/tests/src/Database/DBStructureTest.php index 7b1ff2a31f..152014c114 100644 --- a/tests/src/Database/DBStructureTest.php +++ b/tests/src/Database/DBStructureTest.php @@ -3,25 +3,26 @@ namespace Friendica\Test\src\Database; use Friendica\App; -use Friendica\Core\Config\Cache; use Friendica\Database\DBStructure; use Friendica\Factory; use Friendica\Test\DatabaseTest; use Friendica\Util\BasePath; +use Friendica\Util\Config\ConfigFileLoader; class DBStructureTest extends DatabaseTest { public function setUp() { $basePath = BasePath::create(dirname(__DIR__) . '/../../'); - $configLoader = new Cache\ConfigCacheLoader($basePath); + $mode = new App\Mode($basePath); + $configLoader = new ConfigFileLoader($basePath, $mode); $configCache = Factory\ConfigFactory::createCache($configLoader); $profiler = Factory\ProfilerFactory::create($configCache); Factory\DBFactory::init($basePath, $configCache, $profiler, $_SERVER); $config = Factory\ConfigFactory::createConfig($configCache); Factory\ConfigFactory::createPConfig($configCache); - $logger = Factory\LoggerFactory::create('test', $config); - $this->app = new App($basePath, $config, $logger, $profiler, false); + $logger = Factory\LoggerFactory::create('test', $config, $profiler); + $this->app = new App($config, $mode, $logger, $profiler, false); parent::setUp(); } diff --git a/tests/src/Network/CurlResultTest.php b/tests/src/Network/CurlResultTest.php index bb0bcf2471..b542371b39 100644 --- a/tests/src/Network/CurlResultTest.php +++ b/tests/src/Network/CurlResultTest.php @@ -2,11 +2,20 @@ namespace Friendica\Test\src\Network; +use Friendica\Core\Logger; use Friendica\Network\CurlResult; +use Friendica\Util\Logger\VoidLogger; use PHPUnit\Framework\TestCase; class CurlResultTest extends TestCase { + protected function setUp() + { + parent::setUp(); + + Logger::init(new VoidLogger()); + } + /** * @small */ diff --git a/tests/src/Core/Config/Cache/ConfigCacheLoaderTest.php b/tests/src/Util/Config/ConfigFileLoaderTest.php similarity index 78% rename from tests/src/Core/Config/Cache/ConfigCacheLoaderTest.php rename to tests/src/Util/Config/ConfigFileLoaderTest.php index c9acdfff36..ad0fe8afca 100644 --- a/tests/src/Core/Config/Cache/ConfigCacheLoaderTest.php +++ b/tests/src/Util/Config/ConfigFileLoaderTest.php @@ -1,22 +1,45 @@ setUpVfsDir(); + + $this->mode = \Mockery::mock(App\Mode::class); + $this->mode->shouldReceive('isInstall')->andReturn(true); + } + + /** + * Test the loadConfigFiles() method with default values + */ + public function testLoadConfigFiles() + { + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); + $configCache = new ConfigCache(); + + $configFileLoader->setupCache($configCache); + + $this->assertEquals($this->root->url(), $configCache->get('system', 'basepath')); } /** @@ -32,10 +55,10 @@ class ConfigCacheLoaderTest extends MockedTest ->at($this->root->getChild('config')) ->setContent('root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); $configCache = new ConfigCache(); - $configCacheLoader->loadConfigFiles($configCache); + $configFileLoader->setupCache($configCache); } /** @@ -46,7 +69,6 @@ class ConfigCacheLoaderTest extends MockedTest $this->delConfigFile('local.config.php'); $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'datasets' . DIRECTORY_SEPARATOR . @@ -57,10 +79,10 @@ class ConfigCacheLoaderTest extends MockedTest ->at($this->root->getChild('config')) ->setContent(file_get_contents($file)); - $configCacheLoader = new ConfigCacheLoader($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); $configCache = new ConfigCache(); - $configCacheLoader->loadConfigFiles($configCache); + $configFileLoader->setupCache($configCache); $this->assertEquals('testhost', $configCache->get('database', 'hostname')); $this->assertEquals('testuser', $configCache->get('database', 'username')); @@ -79,7 +101,6 @@ class ConfigCacheLoaderTest extends MockedTest $this->delConfigFile('local.config.php'); $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'datasets' . DIRECTORY_SEPARATOR . @@ -90,10 +111,10 @@ class ConfigCacheLoaderTest extends MockedTest ->at($this->root->getChild('config')) ->setContent(file_get_contents($file)); - $configCacheLoader = new ConfigCacheLoader($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); $configCache = new ConfigCache(); - $configCacheLoader->loadConfigFiles($configCache); + $configFileLoader->setupCache($configCache); $this->assertEquals('testhost', $configCache->get('database', 'hostname')); $this->assertEquals('testuser', $configCache->get('database', 'username')); @@ -111,21 +132,20 @@ class ConfigCacheLoaderTest extends MockedTest $this->delConfigFile('local.config.php'); $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'datasets' . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . - '.htconfig.test.php'; + '.htconfig.php'; vfsStream::newFile('.htconfig.php') ->at($this->root) ->setContent(file_get_contents($file)); - $configCacheLoader = new ConfigCacheLoader($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); $configCache = new ConfigCache(); - $configCacheLoader->loadConfigFiles($configCache); + $configFileLoader->setupCache($configCache); $this->assertEquals('testhost', $configCache->get('database', 'hostname')); $this->assertEquals('testuser', $configCache->get('database', 'username')); @@ -137,7 +157,7 @@ class ConfigCacheLoaderTest extends MockedTest $this->assertEquals('Europe/Berlin', $configCache->get('system', 'default_timezone')); $this->assertEquals('fr', $configCache->get('system', 'language')); - $this->assertEquals('admin@friendica.local', $configCache->get('config', 'admin_email')); + $this->assertEquals('admin@test.it', $configCache->get('config', 'admin_email')); $this->assertEquals('Friendly admin', $configCache->get('config', 'admin_nickname')); $this->assertEquals('/another/php', $configCache->get('config', 'php_path')); @@ -161,7 +181,6 @@ class ConfigCacheLoaderTest extends MockedTest vfsStream::create($structure, $this->root); $file = dirname(__DIR__) . DIRECTORY_SEPARATOR . - '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'datasets' . DIRECTORY_SEPARATOR . @@ -172,9 +191,9 @@ class ConfigCacheLoaderTest extends MockedTest ->at($this->root->getChild('addon')->getChild('test')->getChild('config')) ->setContent(file_get_contents($file)); - $configCacheLoader = new ConfigCacheLoader($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); - $conf = $configCacheLoader->loadAddonConfig('test'); + $conf = $configFileLoader->loadAddonConfig('test'); $this->assertEquals('testhost', $conf['database']['hostname']); $this->assertEquals('testuser', $conf['database']['username']); diff --git a/tests/src/Util/Config/ConfigFileSaverTest.php b/tests/src/Util/Config/ConfigFileSaverTest.php new file mode 100644 index 0000000000..04adf6014a --- /dev/null +++ b/tests/src/Util/Config/ConfigFileSaverTest.php @@ -0,0 +1,189 @@ +setUpVfsDir(); + $this->mode = \Mockery::mock(App\Mode::class); + $this->mode->shouldReceive('isInstall')->andReturn(true); + } + + public function dataConfigFiles() + { + return [ + 'config' => [ + 'fileName' => 'local.config.php', + 'filePath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + 'datasets' . DIRECTORY_SEPARATOR . + 'config', + 'relativePath' => 'config', + ], + 'ini' => [ + 'fileName' => 'local.ini.php', + 'filePath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + 'datasets' . DIRECTORY_SEPARATOR . + 'config', + 'relativePath' => 'config', + ], + 'htconfig' => [ + 'fileName' => '.htconfig.php', + 'filePath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + '..' . DIRECTORY_SEPARATOR . + 'datasets' . DIRECTORY_SEPARATOR . + 'config', + 'relativePath' => '', + ], + ]; + } + + /** + * Test the saveToConfigFile() method + * @dataProvider dataConfigFiles + * + * @todo 20190324 [nupplaphil] for ini-configs, it isn't possible to use $ or ! inside values + */ + public function testSaveToConfig($fileName, $filePath, $relativePath) + { + $this->delConfigFile('local.config.php'); + + if (empty($relativePath)) { + $root = $this->root; + $relativeFullName = $fileName; + } else { + $root = $this->root->getChild($relativePath); + $relativeFullName = $relativePath . DIRECTORY_SEPARATOR . $fileName; + } + + vfsStream::newFile($fileName) + ->at($root) + ->setContent(file_get_contents($filePath . DIRECTORY_SEPARATOR . $fileName)); + + $configFileSaver = new ConfigFileSaver($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); + $configCache = new ConfigCache(); + $configFileLoader->setupCache($configCache); + + $this->assertEquals('admin@test.it', $configCache->get('config', 'admin_email')); + $this->assertEquals('frio', $configCache->get('system', 'theme')); + $this->assertNull($configCache->get('config', 'test_val')); + $this->assertNull($configCache->get('system', 'test_val2')); + + // update values (system and config value) + $configFileSaver->addConfigValue('config', 'admin_email', 'new@mail.it'); + $configFileSaver->addConfigValue('system', 'theme', 'vier'); + + // insert values (system and config value) + $configFileSaver->addConfigValue('config', 'test_val', 'Testingwith@all.we can'); + $configFileSaver->addConfigValue('system', 'test_val2', 'TestIt First'); + + // overwrite value + $configFileSaver->addConfigValue('system', 'test_val2', 'TestIt Now'); + + // save it + $this->assertTrue($configFileSaver->saveToConfigFile()); + + $newConfigCache = new ConfigCache(); + $configFileLoader->setupCache($newConfigCache); + + $this->assertEquals('new@mail.it', $newConfigCache->get('config', 'admin_email')); + $this->assertEquals('Testingwith@all.we can', $newConfigCache->get('config', 'test_val')); + $this->assertEquals('vier', $newConfigCache->get('system', 'theme')); + $this->assertEquals('TestIt Now', $newConfigCache->get('system', 'test_val2')); + + $this->assertTrue($this->root->hasChild($relativeFullName)); + $this->assertTrue($this->root->hasChild($relativeFullName . '.old')); + $this->assertFalse($this->root->hasChild($relativeFullName . '.tmp')); + + $this->assertEquals(file_get_contents($filePath . DIRECTORY_SEPARATOR . $fileName), file_get_contents($this->root->getChild($relativeFullName . '.old')->url())); + } + + /** + * Test the saveToConfigFile() method without permissions + * @dataProvider dataConfigFiles + */ + public function testNoPermission($fileName, $filePath, $relativePath) + { + $this->delConfigFile('local.config.php'); + + if (empty($relativePath)) { + $root = $this->root; + $relativeFullName = $fileName; + } else { + $root = $this->root->getChild($relativePath); + $relativeFullName = $relativePath . DIRECTORY_SEPARATOR . $fileName; + } + + $root->chmod(000); + + vfsStream::newFile($fileName) + ->at($root) + ->setContent(file_get_contents($filePath . DIRECTORY_SEPARATOR . $fileName)); + + $configFileSaver = new ConfigFileSaver($this->root->url()); + + $configFileSaver->addConfigValue('system', 'test_val2', 'TestIt Now'); + + // wrong mod, so return false if nothing to write + $this->assertFalse($configFileSaver->saveToConfigFile()); + } + + /** + * Test the saveToConfigFile() method with nothing to do + * @dataProvider dataConfigFiles + */ + public function testNothingToDo($fileName, $filePath, $relativePath) + { + $this->delConfigFile('local.config.php'); + + if (empty($relativePath)) { + $root = $this->root; + $relativeFullName = $fileName; + } else { + $root = $this->root->getChild($relativePath); + $relativeFullName = $relativePath . DIRECTORY_SEPARATOR . $fileName; + } + + vfsStream::newFile($fileName) + ->at($root) + ->setContent(file_get_contents($filePath . DIRECTORY_SEPARATOR . $fileName)); + + $configFileSaver = new ConfigFileSaver($this->root->url()); + $configFileLoader = new ConfigFileLoader($this->root->url(), $this->mode); + $configCache = new ConfigCache(); + $configFileLoader->setupCache($configCache); + + // save nothing + $this->assertTrue($configFileSaver->saveToConfigFile()); + + $this->assertTrue($this->root->hasChild($relativeFullName)); + $this->assertFalse($this->root->hasChild($relativeFullName . '.old')); + $this->assertFalse($this->root->hasChild($relativeFullName . '.tmp')); + + $this->assertEquals(file_get_contents($filePath . DIRECTORY_SEPARATOR . $fileName), file_get_contents($this->root->getChild($relativeFullName)->url())); + } +} diff --git a/tests/src/Util/Logger/AbstractLoggerTest.php b/tests/src/Util/Logger/AbstractLoggerTest.php new file mode 100644 index 0000000000..e0c0335837 --- /dev/null +++ b/tests/src/Util/Logger/AbstractLoggerTest.php @@ -0,0 +1,143 @@ +introspection = \Mockery::mock(Introspection::class); + $this->introspection->shouldReceive('getRecord')->andReturn([ + 'file' => self::FILE, + 'line' => self::LINE, + 'function' => self::FUNC + ]); + } + + public function assertLogline($string) + { + $this->assertRegExp(self::LOGLINE, $string); + } + + public function assertLoglineNums($assertNum, $string) + { + $this->assertEquals($assertNum, preg_match_all(self::LOGLINE, $string)); + } + + /** + * Test if the logger works correctly + */ + public function testNormal() + { + $logger = $this->getInstance(); + $logger->emergency('working!'); + $logger->alert('working too!'); + $logger->debug('and now?'); + $logger->notice('message', ['an' => 'context']); + + $text = $this->getContent(); + $this->assertLogline($text); + $this->assertLoglineNums(4, $text); + } + + /** + * Test if a log entry is correctly interpolated + */ + public function testPsrInterpolate() + { + $logger = $this->getInstance(); + + $logger->emergency('A {psr} test', ['psr' => 'working']); + $logger->alert('An {array} test', ['array' => ['it', 'is', 'working']]); + $text = $this->getContent(); + $this->assertContains('A working test', $text); + $this->assertContains('An ["it","is","working"] test', $text); + } + + /** + * Test if a log entry contains all necessary information + */ + public function testContainsInformation() + { + $logger = $this->getInstance(); + $logger->emergency('A test'); + + $text = $this->getContent(); + $this->assertContains('"file":"' . self::FILE . '"', $text); + $this->assertContains('"line":' . self::LINE, $text); + $this->assertContains('"function":"' . self::FUNC . '"', $text); + } + + /** + * Test if the minimum level is working + */ + public function testMinimumLevel() + { + $logger = $this->getInstance(LogLevel::NOTICE); + + $logger->emergency('working'); + $logger->alert('working'); + $logger->error('working'); + $logger->warning('working'); + $logger->notice('working'); + $logger->info('not working'); + $logger->debug('not working'); + + $text = $this->getContent(); + + $this->assertLoglineNums(5, $text); + } + + /** + * Test with different logging data + * @dataProvider dataTests + */ + public function testDifferentTypes($function, $message, array $context) + { + $logger = $this->getInstance(); + $logger->$function($message, $context); + + $text = $this->getContent(); + + $this->assertLogline($text); + + $this->assertContains(@json_encode($context), $text); + } +} diff --git a/tests/src/Util/Logger/LoggerDataTrait.php b/tests/src/Util/Logger/LoggerDataTrait.php new file mode 100644 index 0000000000..1267098a88 --- /dev/null +++ b/tests/src/Util/Logger/LoggerDataTrait.php @@ -0,0 +1,52 @@ + [ + 'function' => 'emergency', + 'message' => 'test', + 'context' => ['a' => 'context'], + ], + 'alert' => [ + 'function' => 'alert', + 'message' => 'test {test}', + 'context' => ['a' => 'context', 2 => 'so', 'test' => 'works'], + ], + 'critical' => [ + 'function' => 'critical', + 'message' => 'test crit 2345', + 'context' => ['a' => 'context', 'wit' => ['more', 'array']], + ], + 'error' => [ + 'function' => 'error', + 'message' => 2.554, + 'context' => [], + ], + 'warning' => [ + 'function' => 'warning', + 'message' => 'test warn', + 'context' => ['a' => 'context'], + ], + 'notice' => [ + 'function' => 'notice', + 'message' => 2346, + 'context' => ['a' => 'context'], + ], + 'info' => [ + 'function' => 'info', + 'message' => null, + 'context' => ['a' => 'context'], + ], + 'debug' => [ + 'function' => 'debug', + 'message' => true, + 'context' => ['a' => false], + ], + ]; + } +} diff --git a/tests/src/Util/Logger/ProfilerLoggerTest.php b/tests/src/Util/Logger/ProfilerLoggerTest.php new file mode 100644 index 0000000000..848191a4d6 --- /dev/null +++ b/tests/src/Util/Logger/ProfilerLoggerTest.php @@ -0,0 +1,58 @@ +logger = \Mockery::mock(LoggerInterface::class); + $this->profiler = \Mockery::mock(Profiler::class); + } + + /** + * Test if the profiler is profiling data + * @dataProvider dataTests + */ + public function testProfiling($function, $message, array $context) + { + $logger = new ProfilerLogger($this->logger, $this->profiler); + + $this->logger->shouldReceive($function)->with($message, $context)->once(); + $this->profiler->shouldReceive('saveTimestamp')->with(\Mockery::any(), 'file', \Mockery::any())->once(); + $logger->$function($message, $context); + } + + /** + * Test the log() function + */ + public function testProfilingLog() + { + $logger = new ProfilerLogger($this->logger, $this->profiler); + + $this->logger->shouldReceive('log')->with(LogLevel::WARNING, 'test', ['a' => 'context'])->once(); + $this->profiler->shouldReceive('saveTimestamp')->with(\Mockery::any(), 'file', \Mockery::any())->once(); + + $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); + } +} diff --git a/tests/src/Util/Logger/StreamLoggerTest.php b/tests/src/Util/Logger/StreamLoggerTest.php new file mode 100644 index 0000000000..bbf94419a6 --- /dev/null +++ b/tests/src/Util/Logger/StreamLoggerTest.php @@ -0,0 +1,163 @@ +setUpVfsDir(); + } + + /** + * {@@inheritdoc} + */ + protected function getInstance($level = LogLevel::DEBUG) + { + $this->logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $this->logger = new StreamLogger('test', $this->logfile->url(), $this->introspection, $level); + + return $this->logger; + } + + /** + * {@inheritdoc} + */ + protected function getContent() + { + return $this->logfile->getContent(); + } + + /** + * Test if a stream is working + */ + public function testStream() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $filehandler = fopen($logfile->url(), 'ab'); + + $logger = new StreamLogger('test', $filehandler, $this->introspection); + $logger->emergency('working'); + + $text = $logfile->getContent(); + + $this->assertLogline($text); + } + + /** + * Test if the close statement is working + */ + public function testClose() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + $logger->emergency('working'); + $logger->close(); + // close doesn't affect + $logger->emergency('working too'); + + $text = $logfile->getContent(); + + $this->assertLoglineNums(2, $text); + } + + /** + * Test when a file isn't set + * @expectedException \LogicException + * @expectedExceptionMessage Missing stream URL. + */ + public function testNoUrl() + { + $logger = new StreamLogger('test', '', $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when a file cannot be opened + * @expectedException \UnexpectedValueException + * @expectedExceptionMessageRegExp /The stream or file .* could not be opened: .* / + */ + public function testWrongUrl() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root)->chmod(0); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when the directory cannot get created + * @expectedException \UnexpectedValueException + * @expectedExceptionMessageRegExp /Directory .* cannot get created: .* / + */ + public function testWrongDir() + { + $logger = new StreamLogger('test', '/a/wrong/directory/file.txt', $this->introspection); + + $logger->emergency('not working'); + } + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongMinimumLevel() + { + $logger = new StreamLogger('test', 'file.text', $this->introspection, 'NOPE'); + } + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongLogLevel() + { + $logfile = vfsStream::newFile('friendica.log') + ->at($this->root); + + $logger = new StreamLogger('test', $logfile->url(), $this->introspection); + + $logger->log('NOPE', 'a test'); + } + + /** + * Test when the file is null + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage A stream must either be a resource or a string. + */ + public function testWrongFile() + { + $logger = new StreamLogger('test', null, $this->introspection); + } +} diff --git a/tests/src/Util/Logger/SyslogLoggerTest.php b/tests/src/Util/Logger/SyslogLoggerTest.php new file mode 100644 index 0000000000..aee94e7fdf --- /dev/null +++ b/tests/src/Util/Logger/SyslogLoggerTest.php @@ -0,0 +1,85 @@ +introspection->shouldReceive('addClasses')->with([SyslogLogger::class]); + } + + /** + * {@inheritdoc} + */ + protected function getContent() + { + return $this->logger->getContent(); + } + + /** + * {@inheritdoc} + */ + protected function getInstance($level = LogLevel::DEBUG) + { + $this->logger = new SyslogLoggerWrapper('test', $this->introspection, $level); + + return $this->logger; + } + + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongMinimumLevel() + { + $logger = new SyslogLoggerWrapper('test', $this->introspection, 'NOPE'); + } + + /** + * Test when the minimum level is not valid + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /The level ".*" is not valid./ + */ + public function testWrongLogLevel() + { + $logger = new SyslogLoggerWrapper('test', $this->introspection); + + $logger->log('NOPE', 'a test'); + } + + /** + * Test when the logfacility is wrong (string) + * @expectedException \UnexpectedValueException + * @expectedExceptionMessageRegExp /Can\'t open syslog for ident ".*" and facility ".*": .* / + */ + public function testServerException() + { + $logger = new SyslogLoggerWrapper('test', $this->introspection, LogLevel::DEBUG, null, 'a string'); + $logger->emergency('not working'); + } + + /** + * Test the close() method + */ + public function testClose() + { + $logger = new SyslogLoggerWrapper('test', $this->introspection); + $logger->emergency('test'); + $logger->close(); + // Reopened itself + $logger->emergency('test'); + } +} diff --git a/tests/src/Util/Logger/SyslogLoggerWrapper.php b/tests/src/Util/Logger/SyslogLoggerWrapper.php new file mode 100644 index 0000000000..779067e28e --- /dev/null +++ b/tests/src/Util/Logger/SyslogLoggerWrapper.php @@ -0,0 +1,40 @@ +content = ''; + } + + /** + * Gets the content from the wrapped Syslog + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * {@inheritdoc} + */ + protected function syslogWrapper($level, $entry) + { + $this->content .= $entry . PHP_EOL; + } +} diff --git a/tests/src/Util/Logger/VoidLoggerTest.php b/tests/src/Util/Logger/VoidLoggerTest.php new file mode 100644 index 0000000000..4c436d697a --- /dev/null +++ b/tests/src/Util/Logger/VoidLoggerTest.php @@ -0,0 +1,31 @@ +$function($message, $context); + } + + /** + * Test the log() function + */ + public function testProfilingLog() + { + $logger = new VoidLogger(); + $logger->log(LogLevel::WARNING, 'test', ['a' => 'context']); + } +} diff --git a/update.php b/update.php index e619ec89dd..0ef74ea005 100644 --- a/update.php +++ b/update.php @@ -1,5 +1,6 @@ getBasePath(), $app->getMode())) { + return Update::SUCCESS; + } else { + return Update::FAILED; + } +} diff --git a/view/global.css b/view/global.css index afab5d9032..b48fa1a24a 100644 --- a/view/global.css +++ b/view/global.css @@ -1,6 +1,10 @@ /* General style rules .*/ .pull-right { float: right } +details > summary { + cursor: pointer; +} + /* General designing elements */ .btn { outline: none; diff --git a/view/js/acl.js b/view/js/acl.js index fe0e9f1bbe..d01ffe7cbd 100644 --- a/view/js/acl.js +++ b/view/js/acl.js @@ -225,30 +225,41 @@ ACL.prototype.is_show_all = function() { this.deny_gid.length==0 && this.deny_cid.length==0); }; -ACL.prototype.update_view = function(){ - if (this.is_show_all()){ - this.showall.addClass("selected"); - /* jot acl */ - $('#jot-perms-icon').removeClass('lock').addClass('unlock'); - $('#jot-public').show(); - $('.profile-jot-net input').attr('disabled', false); - if(typeof editor != 'undefined' && editor != false) { - $('#profile-jot-desc').html(ispublic); - } +ACL.prototype.update_view = function () { + if (this.is_show_all()) { + this.showall.addClass("selected"); + /* jot acl */ + $('#jot-perms-icon').removeClass('lock').addClass('unlock'); + $('#jot-public').show(); + $('.profile-jot-net input[type=checkbox]').each(function() { + // Restores checkbox state if it had been saved + if ($(this).attr('data-checked') !== undefined) { + $(this).prop('checked', $(this).attr('data-checked') === 'true'); + } + }); + $('.profile-jot-net input').attr('disabled', false); + if (typeof editor != 'undefined' && editor != false) { + $('#profile-jot-desc').html(ispublic); + } } else { - this.showall.removeClass("selected"); - /* jot acl */ - $('#jot-perms-icon').removeClass('unlock').addClass('lock'); - $('#jot-public').hide(); - $('.profile-jot-net input').attr('disabled', 'disabled'); - $('#profile-jot-desc').html(' '); + this.showall.removeClass("selected"); + /* jot acl */ + $('#jot-perms-icon').removeClass('unlock').addClass('lock'); + $('#jot-public').hide(); + $('.profile-jot-net input[type=checkbox]').each(function() { + // Saves current checkbox state + $(this) + .attr('data-checked', $(this).prop('checked')) + .prop('checked', false); + }); + $('.profile-jot-net input').attr('disabled', 'disabled'); + $('#profile-jot-desc').html(' '); } - $("#acl-list-content .acl-list-item").each(function(){ - $(this).removeClass("groupshow grouphide"); - }); - $("#acl-list-content .acl-list-item").each(function(index, element){ + $("#acl-list-content .acl-list-item").each(function (index, element) { + $(this).removeClass("groupshow grouphide"); + itemid = $(element).attr('id'); type = itemid[0]; id = parseInt(itemid.substr(1)); @@ -256,40 +267,40 @@ ACL.prototype.update_view = function(){ btshow = $(element).children(".acl-button-show").removeClass("selected"); bthide = $(element).children(".acl-button-hide").removeClass("selected"); - switch(type){ + switch (type) { case "g": var uclass = ""; - if (this.allow_gid.indexOf(id)>=0){ + if (this.allow_gid.indexOf(id) >= 0) { btshow.addClass("selected"); bthide.removeClass("selected"); - uclass="groupshow"; + uclass = "groupshow"; } - if (this.deny_gid.indexOf(id)>=0){ + if (this.deny_gid.indexOf(id) >= 0) { btshow.removeClass("selected"); bthide.addClass("selected"); - uclass="grouphide"; + uclass = "grouphide"; } - $(this.group_uids[id]).each(function(i,v) { - if(uclass == "grouphide") - $("#c"+v).removeClass("groupshow"); - if(uclass != "") { - var cls = $("#c"+v).attr('class'); - if( cls == undefined) + $(this.group_uids[id]).each(function (i, v) { + if (uclass == "grouphide") + $("#c" + v).removeClass("groupshow"); + if (uclass != "") { + var cls = $("#c" + v).attr('class'); + if (cls == undefined) return true; var hiding = cls.indexOf('grouphide'); - if(hiding == -1) - $("#c"+v).addClass(uclass); + if (hiding == -1) + $("#c" + v).addClass(uclass); } }); break; case "c": - if (this.allow_cid.indexOf(id)>=0){ + if (this.allow_cid.indexOf(id) >= 0) { btshow.addClass("selected"); bthide.removeClass("selected"); } - if (this.deny_cid.indexOf(id)>=0){ + if (this.deny_cid.indexOf(id) >= 0) { btshow.removeClass("selected"); bthide.addClass("selected"); } @@ -297,7 +308,7 @@ ACL.prototype.update_view = function(){ }.bind(this)); -} +}; ACL.prototype.get = function(start,count, search){ var postdata = { diff --git a/view/templates/acl_selector.tpl b/view/templates/acl_selector.tpl index 48706535f4..6755f2a198 100644 --- a/view/templates/acl_selector.tpl +++ b/view/templates/acl_selector.tpl @@ -19,9 +19,30 @@
      {{$emailcc}}
      -{{if $jotnets}} -{{$jotnets nofilter}} -{{/if}}{{/if}} + + {{if $jotnets_fields}} + {{if $jotnets_fields|count < 3}} +
      + {{else}} +
      + {{$jotnets_summary}} + {{/if}} + + {{foreach $jotnets_fields as $jotnets_field}} + {{if $jotnets_field.type == 'checkbox'}} + {{include file="field_checkbox.tpl" field=$jotnets_field.field}} + {{elseif $jotnets_field.type == 'select'}} + {{include file="field_select.tpl" field=$jotnets_field.field}} + {{/if}} + {{/foreach}} + + {{if $jotnets_fields|count >= 3}} +
      + {{else}} +
      + {{/if}} + {{/if}} +{{/if}}