diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..7692ac78b5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig tool configuration +# see http://editorconfig.org for docs + +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespaces = true +indent_style = tab diff --git a/.gitignore b/.gitignore index 3c0570c67d..c78df3afc1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,8 @@ addon *~ robots.txt -#ignore documentation, it should be newly built -doc/api +#ignore documentation, it should be newly built +doc/html #ignore reports, should be generted with every build report/ @@ -23,7 +23,7 @@ report/ .buildpath .externalToolBuilders .settings -#ignore OSX .DS_Store files +#ignore OSX .DS_Store files .DS_Store /nbproject/private/ @@ -42,3 +42,12 @@ nbproject #ignore local folder /local/ + +#ignore config files from Visual Studio +/.vs/ +/php_friendica.phpproj +/php_friendica.sln +/php_friendica.phpproj.user + +#ignore things from transifex-client +venv/ diff --git a/.htaccess b/.htaccess index 1b63f9ad6f..2348cdc38b 100644 --- a/.htaccess +++ b/.htaccess @@ -4,7 +4,14 @@ AddType audio/ogg .oga #AddHandler php53-cgi .php -Deny from all + + #Apache 2.4 + Require all denied + + + #Apache 2.2 + Deny from all + diff --git a/.tx/config b/.tx/config new file mode 100644 index 0000000000..b42998b861 --- /dev/null +++ b/.tx/config @@ -0,0 +1,9 @@ +[main] +host = https://www.transifex.com + +[friendica.messagespo] +file_filter = view/lang//messages.po +source_file = util/messages.po +source_lang = en +type = PO + diff --git a/CHANGELOG b/CHANGELOG index b393899df8..a3a974eb62 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,131 @@ -Version 3.4.3 +Version 3.5.1 (2017-03-12) + Friendica Core: + Updates to the translations (BG, CA, CS, DE, EO, ES, FR, IS, IT, NL, PL, PT-BR, RU, SV) [translation teams] + Fix for a potential XSS vector [heluecht, thanks to Vít Šesták 'v6ak' for reporting the problem] + Fix for ghost request notifications on single user instances [Hypolite] + Fix user language selection [tobiasd] + Fix a problem with communication to Diaspora with set posting locations [heluecht] + Fix schema handling of direct links to a original posting [Rabuzarus] + Fix a bug in notification handling [Rabuzarus] + Adjustments for the Vagrant VM settings [silke, eelcomaljaars] + Improvements to the unliking of prior likes [Hypolite] + Improvements to the API and Friendica specific extensions [gerhard6380] + Improvements to the Browser Notification functionality [Hypolite] + Improvements to the themes [Hypolite, rabuzarus, rebeka-catalina, tobiasd] + Improvements to the database handling [heluecht] + Improvements to the admin panel [tobiasd, Hypolite] + Improvements to the update process [heluecht] + Improvements to the handling of worker processes [heluecht] + Improvements to the performance [heluecht, Hypolite] + Improvements to the documentation [Hypolite, tobiasd, rabuzarus, beardyunixer, eelcomaljaars] + Improvements to the BBCode / Markdown conversation [Hypolite] + Improvements to the OStatus protocol implementation [heluecht] + Improvements to the installation wizzard [tobiasd] + Improvements to the Diaspora connectivity [heluecht, Hypolite] + Work on PHP7 compatibility [ddorian1] + Code cleanup [Hypolite, Quix0r] + Initial federation with Mastodon [heluecht] + The worker process can now also be started from the frontend [heluecht] + Deletion of postings is now done in the background [heluecht] + Extension of the DFRN transmitted information fields [heluecht] + Translations of the core are now in /view/lang [Hypolite, tobiasd] + Update of the fullCalendar library to 3.0.1 and adjusting the themes [rabuzarus] + ping now works with JSON as well [Hypolite] + On pending registrations, an email is now send to inform the user about it [tobiasd] + On systems where the registration needs approval, a note for the admin can now be written [tobiasd] + Meta Information for HTML descriptions is now limited to 160 character [rabuzarus] + Removed very old deprecated themes from the repository [silke] + Marked frost and frost mobile as deprecated [silke] + When creating new postings in the UI, focus is automatically put into the Title field [Hypolite] + We are now shipping config files for "tx" (the Transifex client) and the "EditorConfig" addon for many common editors [fabrixxm, tobiasd] + The TinyMCE richtext editor was removed [Hypolite] + We defined a coding style, PSR-2 with some adjustments + Various bugfixes + + Friendica Addons: + Updates to the translations (DE, ES, FR, IT, PT-BR) [translation teams] + Improvements to the IFTTT addon [Hypolite] + Improvements to the language filter addon [strk] + Improvements to the pump.io bridge [heluecht] + Improvements to the jappixmini addon [heluecht] + Improvements to the gpluspost addon [heluecht] + Improvements to the performance of the Twitter bridge when using workers [heluecht] + Diaspora Export addon is now working again [heluecht] + Pledgie badge now uses https protocol for embedding [tobiasd] + Better posting loop prevention for the Google+/Twitter/GS connectors [heluecht] + One can now configure the message for wppost bridged blog postings [tobiasd] + On some pages the result of the Rendertime is not shown anymore [heluecht] + Twitter-bridge now supports quotes and long posts when importing tweets [heluecht] + + Closed Issues + 1019, 1163, 1612, 1613, 2103, 2177, 2252, 2260, 2403, 2991, 2614, + 2751, 2752, 2772, 2791, 2800, 2804, 2813, 2814, 2816, 2817, 2823, + 2850, 2858, 2865, 2892, 2894, 2895, 2907, 2908, 2914, 2015, 2926, + 2948, 2955, 2958, 2963, 2964, 2968, 2987, 2993, 3020, 3052, 3062, + 3066, 3091, 3108, 3113, 3116, 3117, 3118, 3126, 3130, 3135, 3155, + 3160, 3163, 3187, 3196 + +Version 3.5 (2016-09-13) + Friendica Core: + NEW Optional local directory with possible federated contacts [heluecht] + NEW Autocompletion for @-mentions and BBCode tags [rabuzarus] + NEW Added a composer derived autoloader which allows composer autoloaders in addons/libraries [fabrixxm] + NEW theme: frio [rabuzarus, heluecht, fabrixxm] + Enhance .htaccess file (nerdoc, dissolve) + Updates to the translations (DE, ES, IS, IT, RU) [translation teams] + Updates to the documentation [tobiasd, heluecht, mexcon, silke, rabuzarus, fabrixxm, Olivier Mehani, gerhard6380, ben utzer] + Extended the BBCode by [abstract] tag used for bridged postings to networks with limited character length [heluecht] + Code cleanup [heluecht, QuixOr] + Improvements to the API and Friendica specific extensions [heluecht, fabrixxm, gerhard6380] + Improvements to the RSS/Atom feed import [mexcon] + Improvements to the communication with federated networks (Diaspora, Hubzilla, OStatus) [heluecht] + Improvements on the themes (quattro, vier, frost) [rabuzarus, fabrixxm, stieben, heluecht, Quix0r, tobiasd] + Improvements to the ACL dialog [fabrixxm, rabuzarus] + Improvements to the database structure and optimization of queries [heluecht] + Improvements to the UI (contacts, hotkeys, remember me, ARIA, code hightlighting) [rabuzarus, heluecht, tobiasd] + Improvements to the background process (poller, worker) [heluecht] + Improvements to the admin panel [tobiasd, heluecht, fabrixxm] + Improvements to the performance [heluecht] + Improvements to the installation wizzard (language selection, RINO version, check required PHP modules, default theme is now vier) [tobiasd] + Improvements to the relocation of nodes and accounts [heluecht] + Improvements to the DDoS detection [heluecht] + Improvements to the calendar/events module [heluecht, rabuzarus] + Improvements to OpenID login [strk] + Improvements to the ShaShape font [andi] + Reworked the implementation of the DFRN, Diaspora protocols [heluecht] + Reworked the notifications code [fabrixxm, rabuzarus, heluecht] + Reworked the p/config code [fabrixxm, rabuzarus] + Reworked XML generation [heluecht] + Removed now unused simplepie from library [heluecht] + + Friendica Addons + Updated to the translations (DE, ES, IS, NL, PT BR), [translation teams] + Piwik [tobiasd] + Twitter Connector [heluecht] + Pumpio Connector [heluecht] + Rendertime [heluecht] + wppost [heluecht] + showmore [rabuzarus] + fromgplus [heluecht] + app.net Connector [heluecht] + GNU Social Connector [heluecht] + LDAP [Olivier Mehani] + smileybutton [rabuzarus] + retriver [mexon] + mailstream [mexon] + forumdirectory [tobiasd] + NEW notifyall (port from Hubzilla) [rabuzarus, tobiasd] + DEPRECATED cal (now in core), FB Connector, FB Post Connector, FB Sync + + Closed Issues + 683, 786, 796, 922, 1261, 1576, 1701, 1769, 1970, 1145, 1494, + 1728, 1877, 2063, 2059, 2078, 2079, 2133, 2165, 2194, 2229, 2230, + 2241, 2254, 2242, 2270, 2277, 2339, 2320, 2345, 2352, 2358, 2367, + 2373, 2376, 2378, 2385, 2395, 2402, 2406, 2433, 2472, 2485, 2492, + 2506, 2512, 2516, 2539, 2540, 2893, 2597, 2611, 2617, 2629, 2645, + 2687, 2716, 2757, 2764 + +Version 3.4.3 (2015-12-22) What's new for the users: Updates to the documentation (silke, tobiasd, annando, rebeka-catalina) Updated translations (tobiasd & translation teams) @@ -77,7 +204,7 @@ Version 3.4.3 Fix bbcode conversion of the about text for the profile (issue #1607) (annando) -Version 3.4.2 +Version 3.4.2 (2015-09-29) Updates to the documentation (tobiasd, silke, annando) Updates to the translations (tobiasd & translation teams) @@ -122,7 +249,7 @@ Version 3.4.2 Parse BBCode in contact request notification email (annando) -Version 3.4.1 +Version 3.4.1 (2015-07-06) Implement server-to-server encryption (RINO) using php-encryption library as "RINO 2", deprecate "RINO 1" (issue #1655) (fabrixxm) Fix connection with Diaspora "freelove" account (issue #1572) (annando) @@ -174,7 +301,7 @@ Version 3.4.1 Update to German documentation (Frank Dieckmann, tobias) Updated translations (translation teams, tobias) -Version 3.4 +Version 3.4 (2015-04-05) Optionally, "like" and "dislike" activities don't update thread timestamp (annando) Updated markdown libraries (annando) @@ -202,7 +329,7 @@ Version 3.4 Add help text to explain the options for approving contacts (issue #1349) (silke) API set as unseen only posts returned by the call (issue #1063) (annando) -Version 3.3.3 +Version 3.3.3 (2015-02-24) More separation between php and html in photo album (issue #1258) (rabuzarus) Enhanced community page shows public posts from public contacts of public profiles (annando) @@ -236,7 +363,7 @@ Version 3.3.3 Fix email validation (ddorian1) Better documentation for developers (silke) -Version 3.3.2 +Version 3.3.2 (2014-12-26) Set default value for all not-null fields (fixes SQL warinigs) (annando) Fix item filters in network page (issue #1222) (fabrixxm) @@ -248,7 +375,7 @@ Version 3.3.2 Better display of pictures in posts (annando) Fix out of control gprobe process (annando) -Version 3.3.1 +Version 3.3.1 (2014-11-06) JSONP support for API (fabrixxm) Fixed small bug in direct messages API (fabrixxm) @@ -266,7 +393,7 @@ Version 3.3.1 Translation updates Added CHANGELOG -Version 3.3 +Version 3.3 (2014-10-06) API added support in the API to allow image uploads from Twidere diff --git a/INSTALL.txt b/INSTALL.txt index 7726bdb0d7..4c57064f6b 100644 --- a/INSTALL.txt +++ b/INSTALL.txt @@ -32,8 +32,7 @@ link if your cert is self-signed). - Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file - - PHP 5.2+. The later the better. PHP 5.3 is required for communications -with the Diaspora network and improved security. + - PHP 5.4+. - PHP *command line* access with register_argc_argv set to true in the php.ini file [or see 'poormancron' in section 8] @@ -42,7 +41,7 @@ php.ini file [or see 'poormancron' in section 8] - some form of email server or email gateway such that PHP mail() works - - Mysql 5.x + - Mysql 5.5.3+ or an equivalant alternative for MySQL (MariaDB, Percona Server etc.) - ability to schedule jobs with cron (Linux/Mac) or Scheduled Tasks (Windows) [Note: other options are presented in Section 8 of this document] @@ -65,7 +64,7 @@ you wish to communicate with the Diaspora network. password, database name). - Friendica needs the permission to create and delete fields and tables in its own database. - + - Please check the additional notes if running on MySQ 5.7.17 or newer 4. If you know in advance that it will be impossible for the web server to write or create files in your web directory, create an empty file called @@ -136,8 +135,25 @@ $a->config['system']['addon'] = 'js_upload,poormancron'; and save your changes. +9. (Optional) Reverse-proxying and HTTPS + +Friendica looks for some well-known HTTP headers indicating a reverse-proxy +terminating an HTTPS connection. While the standard from RFC 7239 specifies +the use of the `Forwaded` header. + + Forwarded: for=192.0.2.1; proto=https; by=192.0.2.2 + +Friendica also supports a number on non-standard headers in common use. + + + X-Forwarded-Proto: https + + Front-End-Https: on + + X-Forwarded-Ssl: on + +It is however preferable to use the standard approach if configuring a new server. - ##################################################################### If things don't work... @@ -275,3 +291,21 @@ This is obvious as soon as you notice that the friendica-cron uses proc_open to execute php-scripts that also use proc_open, but it took me quite some time to find that out. I hope this saves some time for other people using suhosin with function blacklists. + +######################################################################## +Unable to create all mysql tables on MySQL 5.7.17 or newer +####################################################################### + +If the setup fails to create all the database tables and/or manual +creation from the command line fails, with this error: + +ERROR 1067 (42000) at line XX: Invalid default value for 'created' + +You need to adjust your my.cnf and add the following setting under +the [mysqld] section : + +sql_mode = ''; + +After that, restart mysql and try again. + + diff --git a/LICENSE b/LICENSE index 42897de4ab..2bbe2d1de7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Friendica Communications Server -Copyright (c) 2010-2013 the Friendica Project +Copyright (c) 2010-2017 the Friendica Project This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/README.translate.md b/README.translate.md index f434063614..696dec10b1 100644 --- a/README.translate.md +++ b/README.translate.md @@ -4,42 +4,32 @@ Friendica translations Translation Process ------------------- -The strings used in the UI of Friendica is translated at [Transifex] [1] and then -included in the git repository at github. If you want to help with translation -for any language, be it correcting terms or translating friendica to a -currently not supported language, please register an account at transifex.com -and contact the friendica translation team there. +The strings used in the UI of Friendica is translated at [Transifex] [1] and then included in the git repository at github. +If you want to help with translation for any language, be it correcting terms or translating friendica to a currently not supported language, please register an account at transifex.com and contact the friendica translation team there. -Translating friendica is simple. Just use the online tool at transifex. If you -don't want to deal with git & co. that is fine, we check the status of the -translations regularly and import them into the source tree at github so that -others can use them. +Translating friendica is simple. +Just use the online tool at transifex. +If you don't want to deal with git & co. that is fine, we check the status of the translations regularly and import them into the source tree at github so that others can use them. -We do not include every translation from transifex in the source tree to avoid -a scattered and disturbed overall experience. As an uneducated guess we have a -lower limit of 50% translated strings before we include the language (for the -core message.po file, addon translation will be included once all strings of -an addon are translated. This limit is judging only by the amount of translated -strings under the assumption that the most prominent strings for the UI will be -translated first by a translation team. If you feel your translation useable -before this limit, please contact us and we will probably include your teams -work in the source tree. +We do not include every translation from transifex in the source tree to avoid a scattered and disturbed overall experience. +As an uneducated guess we have a lower limit of 50% translated strings before we include the language (for the core messages.po file, addont translation will be included once all strings of an addon are translated. +This limit is judging only by the amount of translated strings under the assumption that the most prominent strings for the UI will be translated first by a translation team. +If you feel your translation useable before this limit, please contact us and we will probably include your teams work in the source tree. -If you want to get your work into the source tree yourself, feel free to do so -and contact us with and question that arises. The process is simple and -friendica ships with all the tools necessary. +If you want to help translating, please concentrate on the core messages.po file first. +We will only include translations with a sufficient translated messages.po file. +Translations of addons will only be included, when the core file is included as well. + +If you want to get your work into the source tree yourself, feel free to do so and contact us with and question that arises. +The process is simple and friendica ships with all the tools necessary. The location of the translated files in the source tree is - /view/LNG-CODE/ + /view/lang/LNG-CODE/ where LNG-CODE is the language code used, e.g. de for German or fr for French. -For the email templates (the *.tpl files) just place them into the directory -and you are done. The translated strings come as a "message.po" file from -transifex which needs to be translated into the PHP file friendica uses. To do -so, place the file in the directory mentioned above and use the "po2php" -utility from the util directory of your friendica installation. +The translated strings come as a "message.po" file from transifex which needs to be translated into the PHP file friendica uses. +To do so, place the file in the directory mentioned above and use the "po2php" utility from the util directory of your friendica installation. -Assuming you want to convert the German localization which is placed in -view/de/message.po you would do the following. +Assuming you want to convert the German localization which is placed in view/lang/de/message.po you would do the following. 1. Navigate at the command prompt to the base directory of your friendica installation @@ -47,20 +37,20 @@ view/de/message.po you would do the following. 2. Execute the po2php script, which will place the translation in the strings.php file that is used by friendica. - $> php util/po2php.php view/de/messages.po + $> php util/po2php.php view/lang/de/messages.po - The output of the script will be placed at view/de/strings.php where + The output of the script will be placed at view/lang/de/strings.php where friendica is expecting it, so you can test your translation immediately. - + 3. Visit your friendica page to check if it still works in the language you just translated. If not try to find the error, most likely PHP will give you a hint in the log/warnings.about the error. - + For debugging you can also try to "run" the file with PHP. This should not give any output if the file is ok but might give a hint for searching the bug in the file. - $> php view/de/strings.php + $> php view/lang/de/strings.php 4. commit the two files with a meaningful commit message to your git repository, push it to your fork of the friendica repository at github and @@ -69,30 +59,39 @@ view/de/message.po you would do the following. Utilities --------- -Additional to the po2php script there are some more utilities for translation -in the "util" directory of the friendica source tree. If you only want to -translate friendica into another language you won't need any of these tools most -likely but it gives you an idea how the translation process of friendica -works. +Additional to the po2php script there are some more utilities for translation in the "util" directory of the friendica source tree. +If you only want to translate friendica into another language you wont need any of these tools most likely but it gives you an idea how the translation process of friendica works. For further information see the utils/README file. -Known Problems --------------- +Transifex-Client +---------------- -Friendica uses the language setting of the visitors browser to determain the -language for the UI. Most of the time this works, but there are some known -quirks. +Transifex has a client program which let you interact with the translation files in a similar way to git. +Help for the client can be found at the [Transifex Help Center] [2]. +Here we will only cover basic usage. -One is that some browsers, like Safari, do the setting to "de-de" but friendica -only has a "de" localisation. A workaround would be to add a symbolic link -from - $friendica/view/de-de -pointing to - $friendica/view/de +After installation of the client, you should have a `tx` command available on your system. +To use it, first create a configuration file with your credentials. +On Linux this file should be placed into your home directory `~/.transifexrc`. +The content of the file should be something like the following: -Links ------ + [https://www.transifex.com] + username = user + token = + password = p@ssw0rd + hostname = https://www.transifex.com + +Since Friendica version 3.5.1 we ship configuration files for the Transifex client in the core repository and the addon repository. +To update the translation files after you have translated strings of e.g. Esperanto in the web-UI of transifex you can use `tx` to download the file. + + $> tx pull -l eo + +And then use the `po2php` utility described above to convert the `messages.po` file to the `strings.php` file Friendica is loading. + + $> php util/po2php.php view/lang/eo/messages.po + +Afterwards, just commit the two changed files to a feature branch of your Friendica repository, push the changes to github and open a pull request for your changes. [1]: https://www.transifex.com/projects/p/friendica/ - +[2]: https://docs.transifex.com/client/introduction diff --git a/VERSION b/VERSION new file mode 100644 index 0000000000..d5c0c99142 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +3.5.1 diff --git a/Vagrantfile b/Vagrantfile index 4f1181b822..ff38151520 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,31 +1,55 @@ -server_ip = "192.168.22.10" -server_memory = "384" # MB +server_ip_trusty = "192.168.22.10" +server_ip_xenial = "192.168.22.11" +server_memory = "1024" # MB server_timezone = "UTC" public_folder = "/vagrant" Vagrant.configure(2) do |config| - +###################################################################### # Set server to Ubuntu 14.04 - config.vm.box = "ubuntu/trusty64" + config.vm.define "trusty" do |trusty| + trusty.vm.box = "ubuntu/trusty64" - # Disable automatic box update checking. If you disable this, then - # boxes will only be checked for updates when the user runs - # `vagrant box outdated`. This is not recommended. - # config.vm.box_check_update = false + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false - # Create a hostname, don't forget to put it to the `hosts` file - # This will point to the server's default virtual host - # TO DO: Make this work with virtualhost along-side xip.io URL - config.vm.hostname = "friendica.dev" + # Create a hostname, don't forget to put it to the `hosts` file + # This will point to the server's default virtual host + # TO DO: Make this work with virtualhost along-side xip.io URL + trusty.vm.hostname = "friendica-trusty.dev" - # Create a static IP - config.vm.network :private_network, ip: server_ip + # Create a static IP + trusty.vm.network :private_network, ip: server_ip_trusty + end +###################################################################### + # Set server to Ubuntu 16.04 + config.vm.define "xenial" do |xenial| + xenial.vm.box = "boxcutter/ubuntu1604" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a hostname, don't forget to put it to the `hosts` file + # This will point to the server's default virtual host + # TO DO: Make this work with virtualhost along-side xip.io URL + xenial.vm.hostname = "friendica-xenial.dev" + + # Create a static IP + xenial.vm.network :private_network, ip: server_ip_xenial + end + +###################################################################### # Share a folder between host and guest config.vm.synced_folder "./", "/vagrant/", owner: "www-data", group: "vagrant" + # Provider-specific configuration so you can fine-tune various # backing providers for Vagrant. These expose provider-specific options. config.vm.provider "virtualbox" do |vb| diff --git a/boot.php b/boot.php index af40027b0d..c8e84e5c75 100644 --- a/boot.php +++ b/boot.php @@ -1,4 +1,25 @@ \r\n" ); define ( 'ATOM_TIME', 'Y-m-d\TH:i:s\Z' ); /** + * @brief Image storage quality. * - * Image storage quality. Lower numbers save space at cost of image detail. + * Lower numbers save space at cost of image detail. * For ease of upgrade, please do not change here. Change jpeg quality with * $a->config['system']['jpeg_quality'] = n; * in .htconfig.php, where n is netween 1 and 100, and with very poor results @@ -66,61 +96,80 @@ define ( 'MAX_IMAGE_LENGTH', -1 ); define ( 'DEFAULT_DB_ENGINE', 'MyISAM' ); /** + * @name SSL Policy + * * SSL redirection policies + * @{ */ - define ( 'SSL_POLICY_NONE', 0 ); define ( 'SSL_POLICY_FULL', 1 ); define ( 'SSL_POLICY_SELFSIGN', 2 ); - +/* @}*/ /** + * @name Logger + * * log levels + * @{ */ - define ( 'LOGGER_NORMAL', 0 ); define ( 'LOGGER_TRACE', 1 ); define ( 'LOGGER_DEBUG', 2 ); define ( 'LOGGER_DATA', 3 ); define ( 'LOGGER_ALL', 4 ); +/* @}*/ /** - * cache levels + * @name Cache + * + * Cache levels + * @{ */ - define ( 'CACHE_MONTH', 0 ); define ( 'CACHE_WEEK', 1 ); define ( 'CACHE_DAY', 2 ); define ( 'CACHE_HOUR', 3 ); +define ( 'CACHE_HALF_HOUR', 4 ); +define ( 'CACHE_QUARTER_HOUR', 5 ); +define ( 'CACHE_FIVE_MINUTES', 6 ); +define ( 'CACHE_MINUTE', 7 ); +/* @}*/ /** - * registration policies + * @name Register + * + * Registration policies + * @{ */ - define ( 'REGISTER_CLOSED', 0 ); define ( 'REGISTER_APPROVE', 1 ); define ( 'REGISTER_OPEN', 2 ); +/** @}*/ /** - * relationship types + * @name Contact_is + * + * Relationship types + * @{ */ - define ( 'CONTACT_IS_FOLLOWER', 1); define ( 'CONTACT_IS_SHARING', 2); define ( 'CONTACT_IS_FRIEND', 3); - +/** @}*/ /** + * @name Update + * * DB update return values + * @{ */ - define ( 'UPDATE_SUCCESS', 0); define ( 'UPDATE_FAILED', 1); +/** @}*/ /** - * - * page/profile types + * @name page/profile types * * PAGE_NORMAL is a typical personal profile account * PAGE_SOAPBOX automatically approves all friend requests as CONTACT_IS_SHARING, (readonly) @@ -128,24 +177,55 @@ define ( 'UPDATE_FAILED', 1); * write access to wall and comments (no email and not included in page owner's ACL lists) * PAGE_FREELOVE automatically approves all friend requests as full friends (CONTACT_IS_FRIEND). * + * @{ */ - define ( 'PAGE_NORMAL', 0 ); define ( 'PAGE_SOAPBOX', 1 ); define ( 'PAGE_COMMUNITY', 2 ); define ( 'PAGE_FREELOVE', 3 ); define ( 'PAGE_BLOG', 4 ); define ( 'PAGE_PRVGROUP', 5 ); +/** @}*/ -// Type of the community page +/** + * @name account types + * + * ACCOUNT_TYPE_PERSON - the account belongs to a person + * Associated page types: PAGE_NORMAL, PAGE_SOAPBOX, PAGE_FREELOVE + * + * ACCOUNT_TYPE_ORGANISATION - the account belongs to an organisation + * Associated page type: PAGE_SOAPBOX + * + * ACCOUNT_TYPE_NEWS - the account is a news reflector + * Associated page type: PAGE_SOAPBOX + * + * ACCOUNT_TYPE_COMMUNITY - the account is community forum + * Associated page types: PAGE_COMMUNITY, PAGE_PRVGROUP + * @{ + */ +define ( 'ACCOUNT_TYPE_PERSON', 0 ); +define ( 'ACCOUNT_TYPE_ORGANISATION',1 ); +define ( 'ACCOUNT_TYPE_NEWS', 2 ); +define ( 'ACCOUNT_TYPE_COMMUNITY', 3 ); +/** @}*/ + +/** + * @name CP + * + * Type of the community page + * @{ + */ define ( 'CP_NO_COMMUNITY_PAGE', -1 ); define ( 'CP_USERS_ON_SERVER', 0 ); define ( 'CP_GLOBAL_COMMUNITY', 1 ); +/** @}*/ /** + * @name Network + * * Network and protocol family types + * @{ */ - define ( 'NETWORK_DFRN', 'dfrn'); // Friendica, Mistpark, other DFRN implementations define ( 'NETWORK_ZOT', 'zot!'); // Zot! define ( 'NETWORK_OSTATUS', 'stat'); // status.net, identi.ca, GNU-social, other OStatus implementations @@ -165,7 +245,9 @@ define ( 'NETWORK_STATUSNET', 'stac'); // Statusnet connector define ( 'NETWORK_APPNET', 'apdn'); // app.net define ( 'NETWORK_NEWS', 'nntp'); // Network News Transfer Protocol define ( 'NETWORK_ICALENDAR', 'ical'); // iCalendar +define ( 'NETWORK_PNUT', 'pnut'); // pnut.io define ( 'NETWORK_PHANTOM', 'unkn'); // Place holder +/** @}*/ /** * These numbers are used in stored permissions @@ -193,6 +275,7 @@ $netgroup_ids = array( NETWORK_APPNET => (-17), NETWORK_NEWS => (-18), NETWORK_ICALENDAR => (-19), + NETWORK_PNUT => (-20), NETWORK_PHANTOM => (-127), ); @@ -212,9 +295,11 @@ define ( 'ZCURL_TIMEOUT' , (-1)); /** - * email notification options + * @name Notify + * + * Email notification options + * @{ */ - define ( 'NOTIFY_INTRO', 0x0001 ); define ( 'NOTIFY_CONFIRM', 0x0002 ); define ( 'NOTIFY_WALL', 0x0004 ); @@ -228,12 +313,15 @@ define ( 'NOTIFY_POKE', 0x0200 ); define ( 'NOTIFY_SHARE', 0x0400 ); define ( 'NOTIFY_SYSTEM', 0x8000 ); +/* @}*/ /** + * @name Term + * * Tag/term types + * @{ */ - define ( 'TERM_UNKNOWN', 0 ); define ( 'TERM_HASHTAG', 1 ); define ( 'TERM_MENTION', 2 ); @@ -249,9 +337,11 @@ define ( 'TERM_OBJ_PHOTO', 2 ); /** - * various namespaces we may need to parse + * @name Namespaces + * + * Various namespaces we may need to parse + * @{ */ - define ( 'NAMESPACE_ZOT', 'http://purl.org/zot' ); define ( 'NAMESPACE_DFRN' , 'http://purl.org/macgirvin/dfrn/1.0' ); define ( 'NAMESPACE_THREAD' , 'http://purl.org/syndication/thread/1.0' ); @@ -267,10 +357,14 @@ define ( 'NAMESPACE_FEED', 'http://schemas.google.com/g/2010#updates- define ( 'NAMESPACE_OSTATUS', 'http://ostatus.org/schema/1.0' ); define ( 'NAMESPACE_STATUSNET', 'http://status.net/schema/api/1/' ); define ( 'NAMESPACE_ATOM1', 'http://www.w3.org/2005/Atom' ); -/** - * activity stream defines - */ +/* @}*/ +/** + * @name Activity + * + * Activity stream defines + * @{ + */ define ( 'ACTIVITY_LIKE', NAMESPACE_ACTIVITY_SCHEMA . 'like' ); define ( 'ACTIVITY_DISLIKE', NAMESPACE_DFRN . '/dislike' ); define ( 'ACTIVITY_ATTEND', NAMESPACE_ZOT . '/activity/attendyes' ); @@ -309,14 +403,48 @@ define ( 'ACTIVITY_OBJ_GROUP', NAMESPACE_ACTIVITY_SCHEMA . 'group' ); define ( 'ACTIVITY_OBJ_TAGTERM', NAMESPACE_DFRN . '/tagterm' ); define ( 'ACTIVITY_OBJ_PROFILE', NAMESPACE_DFRN . '/profile' ); define ( 'ACTIVITY_OBJ_QUESTION', 'http://activityschema.org/object/question' ); +/* @}*/ /** - * item weight for query ordering + * @name Gravity + * + * Item weight for query ordering + * @{ */ - define ( 'GRAVITY_PARENT', 0); define ( 'GRAVITY_LIKE', 3); define ( 'GRAVITY_COMMENT', 6); +/* @}*/ + +/** + * @name Priority + * + * Process priority for the worker + * @{ + */ +define('PRIORITY_UNDEFINED', 0); +define('PRIORITY_CRITICAL', 10); +define('PRIORITY_HIGH', 20); +define('PRIORITY_MEDIUM', 30); +define('PRIORITY_LOW', 40); +define('PRIORITY_NEGLIGIBLE',50); +/* @}*/ + +/** + * @name Social Relay settings + * + * See here: https://github.com/jaywink/social-relay + * and here: https://wiki.diasporafoundation.org/Relay_servers_for_public_posts + * @{ + */ +define('SR_SCOPE_NONE', ''); +define('SR_SCOPE_ALL', 'all'); +define('SR_SCOPE_TAGS', 'tags'); +/* @}*/ + +// Normally this constant is defined - but not if "pcntl" isn't installed +if (!defined("SIGTERM")) + define("SIGTERM", 15); /** * @@ -358,7 +486,8 @@ function startup() { * * class: App * - * Our main application structure for the life of this page + * @brief Our main application structure for the life of this page. + * * Primarily deals with the URL that got us here * and tries to make some sense of it, and * stores our page contents and config storage @@ -366,667 +495,1075 @@ function startup() { * before we spit the page out. * */ - -if(! class_exists('App')) { - class App { - - public $module_loaded = false; - public $query_string; - public $config; - public $page; - public $profile; - public $profile_uid; - public $user; - public $cid; - public $contact; - public $contacts; - public $page_contact; - public $content; - public $data = array(); - public $error = false; - public $cmd; - public $argv; - public $argc; - public $module; - public $pager; - public $strings; - public $path; - public $hooks; - public $timezone; - public $interactive = true; - public $plugins; - public $apps = array(); - public $identities; - public $is_mobile; - public $is_tablet; - public $is_friendica_app; - public $performance = array(); - - public $nav_sel; - - public $category; - - - // Allow themes to control internal parameters - // by changing App values in theme.php - - public $sourcename = ''; - public $videowidth = 425; - public $videoheight = 350; - public $force_max_items = 0; - public $theme_thread_allow = true; - public $theme_events_in_profile = true; - - // An array for all theme-controllable parameters - // Mostly unimplemented yet. Only options 'stylesheet' and - // beyond are used. - - public $theme = array( - 'sourcename' => '', - 'videowidth' => 425, - 'videoheight' => 350, - 'force_max_items' => 0, - 'thread_allow' => true, - 'stylesheet' => '', - 'template_engine' => 'smarty3', - ); - - // array of registered template engines ('name'=>'class name') - public $template_engines = array(); - // array of instanced template engines ('name'=>'instance') - public $template_engine_instance = array(); - - private $ldelim = array( - 'internal' => '', - 'smarty3' => '{{' - ); - private $rdelim = array( - 'internal' => '', - 'smarty3' => '}}' - ); - - private $scheme; - private $hostname; - private $baseurl; - private $db; - - private $curl_code; - private $curl_content_type; - private $curl_headers; - - private $cached_profile_image; - private $cached_profile_picdate; - - function __construct() { - - global $default_timezone; - - $hostname = ""; - - if (file_exists(".htpreconfig.php")) - @include(".htpreconfig.php"); - - $this->timezone = ((x($default_timezone)) ? $default_timezone : 'UTC'); - - date_default_timezone_set($this->timezone); - - $this->performance["start"] = microtime(true); - $this->performance["database"] = 0; - $this->performance["network"] = 0; - $this->performance["file"] = 0; - $this->performance["rendering"] = 0; - $this->performance["parser"] = 0; - $this->performance["marktime"] = 0; - $this->performance["markstart"] = microtime(true); - - $this->config = array(); - $this->page = array(); - $this->pager= array(); - - $this->query_string = ''; - - startup(); - - set_include_path( - 'include' . PATH_SEPARATOR - . 'library' . PATH_SEPARATOR - . 'library/phpsec' . PATH_SEPARATOR - . 'library/langdet' . PATH_SEPARATOR - . '.' ); - - - $this->scheme = 'http'; - if(x($_SERVER,'HTTPS') && $_SERVER['HTTPS']) - $this->scheme = 'https'; - elseif(x($_SERVER,'SERVER_PORT') && (intval($_SERVER['SERVER_PORT']) == 443)) - $this->scheme = 'https'; - - if(x($_SERVER,'SERVER_NAME')) { - $this->hostname = $_SERVER['SERVER_NAME']; - - // See bug 437 - this didn't work so disabling it - //if(stristr($this->hostname,'xn--')) { - // PHP or webserver may have converted idn to punycode, so - // convert punycode back to utf-8 - // require_once('library/simplepie/idn/idna_convert.class.php'); - // $x = new idna_convert(); - // $this->hostname = $x->decode($_SERVER['SERVER_NAME']); - //} - - if(x($_SERVER,'SERVER_PORT') && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) - $this->hostname .= ':' . $_SERVER['SERVER_PORT']; - /** - * Figure out if we are running at the top of a domain - * or in a sub-directory and adjust accordingly - */ - - $path = trim(dirname($_SERVER['SCRIPT_NAME']),'/\\'); - if(isset($path) && strlen($path) && ($path != $this->path)) - $this->path = $path; - } - - if ($hostname != "") - $this->hostname = $hostname; - - if (is_array($_SERVER["argv"]) && $_SERVER["argc"]>1 && substr(end($_SERVER["argv"]), 0, 4)=="http" ) { - $this->set_baseurl(array_pop($_SERVER["argv"]) ); - $_SERVER["argc"] --; - } - - #set_include_path("include/$this->hostname" . PATH_SEPARATOR . get_include_path()); - - if((x($_SERVER,'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'],0,9) === "pagename=") { - $this->query_string = substr($_SERVER['QUERY_STRING'],9); - // removing trailing / - maybe a nginx problem - if (substr($this->query_string, 0, 1) == "/") - $this->query_string = substr($this->query_string, 1); - } elseif((x($_SERVER,'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'],0,2) === "q=") { - $this->query_string = substr($_SERVER['QUERY_STRING'],2); - // removing trailing / - maybe a nginx problem - if (substr($this->query_string, 0, 1) == "/") - $this->query_string = substr($this->query_string, 1); - } - - if (x($_GET,'pagename')) - $this->cmd = trim($_GET['pagename'],'/\\'); - elseif (x($_GET,'q')) - $this->cmd = trim($_GET['q'],'/\\'); - - - // fix query_string - $this->query_string = str_replace($this->cmd."&",$this->cmd."?", $this->query_string); - - - // unix style "homedir" - - if(substr($this->cmd,0,1) === '~') - $this->cmd = 'profile/' . substr($this->cmd,1); - - // Diaspora style profile url - - if(substr($this->cmd,0,2) === 'u/') - $this->cmd = 'profile/' . substr($this->cmd,2); - - - /** - * - * Break the URL path into C style argc/argv style arguments for our - * modules. Given "http://example.com/module/arg1/arg2", $this->argc - * will be 3 (integer) and $this->argv will contain: - * [0] => 'module' - * [1] => 'arg1' - * [2] => 'arg2' - * - * - * There will always be one argument. If provided a naked domain - * URL, $this->argv[0] is set to "home". - * - */ - - $this->argv = explode('/',$this->cmd); - $this->argc = count($this->argv); - if((array_key_exists('0',$this->argv)) && strlen($this->argv[0])) { - $this->module = str_replace(".", "_", $this->argv[0]); - $this->module = str_replace("-", "_", $this->module); - } - else { - $this->argc = 1; - $this->argv = array('home'); - $this->module = 'home'; - } - - /** - * See if there is any page number information, and initialise - * pagination - */ - - $this->pager['page'] = ((x($_GET,'page') && intval($_GET['page']) > 0) ? intval($_GET['page']) : 1); - $this->pager['itemspage'] = 50; - $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; - if($this->pager['start'] < 0) - $this->pager['start'] = 0; - $this->pager['total'] = 0; - - /** - * Detect mobile devices - */ - - $mobile_detect = new Mobile_Detect(); - $this->is_mobile = $mobile_detect->isMobile(); - $this->is_tablet = $mobile_detect->isTablet(); - - // Friendica-Client - $this->is_friendica_app = ($_SERVER['HTTP_USER_AGENT'] == "Apache-HttpClient/UNAVAILABLE (java 1.4)"); - - /** - * register template engines - */ - $dc = get_declared_classes(); - foreach ($dc as $k) { - if (in_array("ITemplateEngine", class_implements($k))){ - $this->register_template_engine($k); - } - } - - } - - function get_basepath() { - - $basepath = get_config("system", "basepath"); - - if ($basepath == "") - $basepath = dirname(__FILE__); - - if ($basepath == "") - $basepath = $_SERVER["DOCUMENT_ROOT"]; - - if ($basepath == "") - $basepath = $_SERVER["PWD"]; - - return($basepath); - } - - function get_scheme() { - return($this->scheme); - } - - function get_baseurl($ssl = false) { - - $scheme = $this->scheme; - - if((x($this->config,'system')) && (x($this->config['system'],'ssl_policy'))) { - if(intval($this->config['system']['ssl_policy']) === intval(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($this->config['system']['ssl_policy'] == SSL_POLICY_SELFSIGN) { - if($ssl) - $scheme = 'https'; - else - $scheme = 'http'; - } - } - - if (get_config('config','hostname') != "") - $this->hostname = get_config('config','hostname'); - - $this->baseurl = $scheme . "://" . $this->hostname . ((isset($this->path) && strlen($this->path)) ? '/' . $this->path : '' ); - return $this->baseurl; - } - - function set_baseurl($url) { - $parsed = @parse_url($url); - - $this->baseurl = $url; - - if($parsed) { - $this->scheme = $parsed['scheme']; - - $hostname = $parsed['host']; - if(x($parsed,'port')) - $hostname .= ':' . $parsed['port']; - if(x($parsed,'path')) - $this->path = trim($parsed['path'],'\\/'); - - if (file_exists(".htpreconfig.php")) - @include(".htpreconfig.php"); - - if (get_config('config','hostname') != "") - $this->hostname = get_config('config','hostname'); - - if (!isset($this->hostname) OR ($this->hostname == "")) - $this->hostname = $hostname; - } - - } - - function get_hostname() { - if (get_config('config','hostname') != "") - $this->hostname = get_config('config','hostname'); - - return $this->hostname; - } - - function set_hostname($h) { - $this->hostname = $h; - } - - function set_path($p) { - $this->path = trim(trim($p),'/'); - } - - function get_path() { - return $this->path; - } - - function set_pager_total($n) { - $this->pager['total'] = intval($n); - } - - function set_pager_itemspage($n) { - $this->pager['itemspage'] = ((intval($n) > 0) ? intval($n) : 0); - $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; - } - - function set_pager_page($n) { - $this->pager['page'] = $n; - $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; - } - - function init_pagehead() { - $interval = ((local_user()) ? get_pconfig(local_user(),'system','update_interval') : 40000); - - // If the update is "deactivated" set it to the highest integer number (~24 days) - if ($interval < 0) - $interval = 2147483647; - - if($interval < 10000) - $interval = 40000; - - // compose the page title from the sitename and the - // current module called - if (!$this->module=='') - { - $this->page['title'] = $this->config['sitename'].' ('.$this->module.')'; - } else { - $this->page['title'] = $this->config['sitename']; - } - - /* put the head template at the beginning of page['htmlhead'] - * since the code added by the modules frequently depends on it - * being first - */ - if(!isset($this->page['htmlhead'])) - $this->page['htmlhead'] = ''; - - // If we're using Smarty, then doing replace_macros() will replace - // any unrecognized variables with a blank string. Since we delay - // replacing $stylesheet until later, we need to replace it now - // with another variable name - if($this->theme['template_engine'] === 'smarty3') - $stylesheet = $this->get_template_ldelim('smarty3') . '$stylesheet' . $this->get_template_rdelim('smarty3'); - else - $stylesheet = '$stylesheet'; - - $shortcut_icon = get_config("system", "shortcut_icon"); - if ($shortcut_icon == "") - $shortcut_icon = $this->get_baseurl()."/images/friendica-32.png"; - - $touch_icon = get_config("system", "touch_icon"); - if ($touch_icon == "") - $touch_icon = $this->get_baseurl()."/images/friendica-128.png"; - - $tpl = get_markup_template('head.tpl'); - $this->page['htmlhead'] = replace_macros($tpl,array( - '$baseurl' => $this->get_baseurl(), // FIXME for z_path!!!! - '$local_user' => local_user(), - '$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION, - '$delitem' => t('Delete this item?'), - '$comment' => t('Comment'), - '$showmore' => t('show more'), - '$showfewer' => t('show fewer'), - '$update_interval' => $interval, - '$shortcut_icon' => $shortcut_icon, - '$touch_icon' => $touch_icon, - '$stylesheet' => $stylesheet - )) . $this->page['htmlhead']; - } - - function init_page_end() { - if(!isset($this->page['end'])) - $this->page['end'] = ''; - $tpl = get_markup_template('end.tpl'); - $this->page['end'] = replace_macros($tpl,array( - '$baseurl' => $this->get_baseurl() // FIXME for z_path!!!! - )) . $this->page['end']; - } - - function set_curl_code($code) { - $this->curl_code = $code; - } - - function get_curl_code() { - return $this->curl_code; - } - - function set_curl_content_type($content_type) { - $this->curl_content_type = $content_type; - } - - function get_curl_content_type() { - return $this->curl_content_type; - } - - function set_curl_headers($headers) { - $this->curl_headers = $headers; - } - - function get_curl_headers() { - return $this->curl_headers; - } - - function get_cached_avatar_image($avatar_image){ - if($this->cached_profile_image[$avatar_image]) - return $this->cached_profile_image[$avatar_image]; - - $path_parts = explode("/",$avatar_image); - $common_filename = $path_parts[count($path_parts)-1]; - - if($this->cached_profile_picdate[$common_filename]){ - $this->cached_profile_image[$avatar_image] = $avatar_image . $this->cached_profile_picdate[$common_filename]; - } else { - $r = q("SELECT `contact`.`avatar-date` AS picdate FROM `contact` WHERE `contact`.`thumb` like '%%/%s'", - $common_filename); - if(! count($r)){ - $this->cached_profile_image[$avatar_image] = $avatar_image; - } else { - $this->cached_profile_picdate[$common_filename] = "?rev=".urlencode($r[0]['picdate']); - $this->cached_profile_image[$avatar_image] = $avatar_image.$this->cached_profile_picdate[$common_filename]; - } - } - return $this->cached_profile_image[$avatar_image]; - } - - - /** - * register template engine class - * if $name is "", is used class static property $class::$name - * @param string $class - * @param string $name - */ - function register_template_engine($class, $name = '') { - if ($name===""){ - $v = get_class_vars( $class ); - if(x($v,"name")) $name = $v['name']; - } - if ($name===""){ - echo "template engine $class cannot be registered without a name.\n"; - killme(); - } - $this->template_engines[$name] = $class; - } - - /** - * return template engine instance. If $name is not defined, - * return engine defined by theme, or default - * - * @param strin $name Template engine name - * @return object Template Engine instance - */ - function template_engine($name = ''){ - if ($name!=="") { - $template_engine = $name; - } else { - $template_engine = 'smarty3'; - if (x($this->theme, 'template_engine')) { - $template_engine = $this->theme['template_engine']; - } - } - - if (isset($this->template_engines[$template_engine])){ - if(isset($this->template_engine_instance[$template_engine])){ - return $this->template_engine_instance[$template_engine]; - } else { - $class = $this->template_engines[$template_engine]; - $obj = new $class; - $this->template_engine_instance[$template_engine] = $obj; - return $obj; - } - } - - echo "template engine $template_engine is not registered!\n"; killme(); - } - - function get_template_engine() { - return $this->theme['template_engine']; - } - - function set_template_engine($engine = 'smarty3') { - $this->theme['template_engine'] = $engine; +class App { + + public $module_loaded = false; + public $query_string; + public $config; + public $page; + public $profile; + public $profile_uid; + public $user; + public $cid; + public $contact; + public $contacts; + public $page_contact; + public $content; + public $data = array(); + public $error = false; + public $cmd; + public $argv; + public $argc; + public $module; + public $pager; + public $strings; + public $path; + public $hooks; + public $timezone; + public $interactive = true; + public $plugins; + public $apps = array(); + public $identities; + public $is_mobile = false; + public $is_tablet = false; + public $is_friendica_app; + public $performance = array(); + public $callstack = array(); + public $theme_info = array(); + public $backend = true; + + public $nav_sel; + + public $category; + + + // Allow themes to control internal parameters + // by changing App values in theme.php + + public $sourcename = ''; + public $videowidth = 425; + public $videoheight = 350; + public $force_max_items = 0; + public $theme_thread_allow = true; + public $theme_events_in_profile = true; + + /** + * @brief An array for all theme-controllable parameters + * + * Mostly unimplemented yet. Only options 'template_engine' and + * beyond are used. + */ + public $theme = array( + 'sourcename' => '', + 'videowidth' => 425, + 'videoheight' => 350, + 'force_max_items' => 0, + 'thread_allow' => true, + 'stylesheet' => '', + 'template_engine' => 'smarty3', + ); + + /** + * @brief An array of registered template engines ('name'=>'class name') + */ + public $template_engines = array(); + /** + * @brief An array of instanced template engines ('name'=>'instance') + */ + public $template_engine_instance = array(); + + public $process_id; + + private $ldelim = array( + 'internal' => '', + 'smarty3' => '{{' + ); + private $rdelim = array( + 'internal' => '', + 'smarty3' => '}}' + ); + + private $scheme; + private $hostname; + private $db; + + private $curl_code; + private $curl_content_type; + private $curl_headers; + + private $cached_profile_image; + private $cached_profile_picdate; + + private static $a; + + /** + * @brief App constructor. + */ + function __construct() { + + global $default_timezone; + + $hostname = ""; + + if (file_exists(".htpreconfig.php")) + @include(".htpreconfig.php"); + + $this->timezone = ((x($default_timezone)) ? $default_timezone : 'UTC'); + + date_default_timezone_set($this->timezone); + + $this->performance["start"] = microtime(true); + $this->performance["database"] = 0; + $this->performance["database_write"] = 0; + $this->performance["network"] = 0; + $this->performance["file"] = 0; + $this->performance["rendering"] = 0; + $this->performance["parser"] = 0; + $this->performance["marktime"] = 0; + $this->performance["markstart"] = microtime(true); + + $this->callstack["database"] = array(); + $this->callstack["database_write"] = array(); + $this->callstack["network"] = array(); + $this->callstack["file"] = array(); + $this->callstack["rendering"] = array(); + $this->callstack["parser"] = array(); + + $this->config = array(); + $this->page = array(); + $this->pager= array(); + + $this->query_string = ''; + + $this->process_id = uniqid("log", true); + + startup(); + + set_include_path( + 'include' . PATH_SEPARATOR + . 'library' . PATH_SEPARATOR + . 'library/phpsec' . PATH_SEPARATOR + . 'library/langdet' . PATH_SEPARATOR + . '.' ); + + + $this->scheme = 'http'; + if((x($_SERVER,'HTTPS') && $_SERVER['HTTPS']) || + (x($_SERVER['HTTP_FORWARDED']) && preg_match("/proto=https/", $_SERVER['HTTP_FORWARDED'])) || + (x($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') || + (x($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') || + (x($_SERVER['FRONT_END_HTTPS']) && $_SERVER['FRONT_END_HTTPS'] == 'on') || + (x($_SERVER,'SERVER_PORT') && (intval($_SERVER['SERVER_PORT']) == 443)) // XXX: reasonable assumption, but isn't this hardcoding too much? + ) { + $this->scheme = 'https'; + } + + if(x($_SERVER,'SERVER_NAME')) { + $this->hostname = $_SERVER['SERVER_NAME']; + + if(x($_SERVER,'SERVER_PORT') && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) + $this->hostname .= ':' . $_SERVER['SERVER_PORT']; /* - $this->theme['template_engine'] = 'smarty3'; + * Figure out if we are running at the top of a domain + * or in a sub-directory and adjust accordingly + */ - switch($engine) { - case 'smarty3': - if(is_writable('view/smarty3/')) - $this->theme['template_engine'] = 'smarty3'; - break; - default: - break; - } - */ + $path = trim(dirname($_SERVER['SCRIPT_NAME']),'/\\'); + if(isset($path) && strlen($path) && ($path != $this->path)) + $this->path = $path; } - function get_template_ldelim($engine = 'smarty3') { - return $this->ldelim[$engine]; + if ($hostname != "") + $this->hostname = $hostname; + + if (is_array($_SERVER["argv"]) && $_SERVER["argc"]>1 && substr(end($_SERVER["argv"]), 0, 4)=="http" ) { + $this->set_baseurl(array_pop($_SERVER["argv"]) ); + $_SERVER["argc"] --; } - function get_template_rdelim($engine = 'smarty3') { - return $this->rdelim[$engine]; + #set_include_path("include/$this->hostname" . PATH_SEPARATOR . get_include_path()); + + if ((x($_SERVER,'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'],0,9) === "pagename=") { + $this->query_string = substr($_SERVER['QUERY_STRING'],9); + // removing trailing / - maybe a nginx problem + if (substr($this->query_string, 0, 1) == "/") + $this->query_string = substr($this->query_string, 1); + } elseif ((x($_SERVER,'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'],0,2) === "q=") { + $this->query_string = substr($_SERVER['QUERY_STRING'],2); + // removing trailing / - maybe a nginx problem + if (substr($this->query_string, 0, 1) == "/") + $this->query_string = substr($this->query_string, 1); } - function save_timestamp($stamp, $value) { - $duration = (float)(microtime(true)-$stamp); - - $this->performance[$value] += (float)$duration; - $this->performance["marktime"] += (float)$duration; + if (x($_GET,'pagename')) { + $this->cmd = trim($_GET['pagename'],'/\\'); + } elseif (x($_GET,'q')) { + $this->cmd = trim($_GET['q'],'/\\'); } - function mark_timestamp($mark) { - //$this->performance["markstart"] -= microtime(true) - $this->performance["marktime"]; - $this->performance["markstart"] = microtime(true) - $this->performance["markstart"] - $this->performance["marktime"]; + + // fix query_string + $this->query_string = str_replace($this->cmd."&",$this->cmd."?", $this->query_string); + + + // unix style "homedir" + + if (substr($this->cmd,0,1) === '~') { + $this->cmd = 'profile/' . substr($this->cmd,1); } - function get_useragent() { - return(FRIENDICA_PLATFORM." '".FRIENDICA_CODENAME."' ".FRIENDICA_VERSION."-".DB_UPDATE_VERSION."; ".$this->get_baseurl()); + // Diaspora style profile url + + if (substr($this->cmd,0,2) === 'u/') { + $this->cmd = 'profile/' . substr($this->cmd,2); } - function is_friendica_app() { - return($this->is_friendica_app); - } - } -} + /* + * + * Break the URL path into C style argc/argv style arguments for our + * modules. Given "http://example.com/module/arg1/arg2", $this->argc + * will be 3 (integer) and $this->argv will contain: + * [0] => 'module' + * [1] => 'arg1' + * [2] => 'arg2' + * + * + * There will always be one argument. If provided a naked domain + * URL, $this->argv[0] is set to "home". + * + */ -// retrieve the App structure -// useful in functions which require it but don't get it passed to them - -if(! function_exists('get_app')) { - function get_app() { - global $a; - return $a; - } -}; - - -// Multi-purpose function to check variable state. -// Usage: x($var) or $x($array,'key') -// returns false if variable/key is not set -// if variable is set, returns 1 if has 'non-zero' value, otherwise returns 0. -// e.g. x('') or x(0) returns 0; - -if(! function_exists('x')) { - function x($s,$k = NULL) { - if($k != NULL) { - if((is_array($s)) && (array_key_exists($k,$s))) { - if($s[$k]) - return (int) 1; - return (int) 0; - } - return false; + $this->argv = explode('/',$this->cmd); + $this->argc = count($this->argv); + if((array_key_exists('0',$this->argv)) && strlen($this->argv[0])) { + $this->module = str_replace(".", "_", $this->argv[0]); + $this->module = str_replace("-", "_", $this->module); } else { - if(isset($s)) { - if($s) { - return (int) 1; - } - return (int) 0; + $this->argc = 1; + $this->argv = array('home'); + $this->module = 'home'; + } + + /* + * See if there is any page number information, and initialise + * pagination + */ + + $this->pager['page'] = ((x($_GET,'page') && intval($_GET['page']) > 0) ? intval($_GET['page']) : 1); + $this->pager['itemspage'] = 50; + $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; + if($this->pager['start'] < 0) + $this->pager['start'] = 0; + $this->pager['total'] = 0; + + /* + * Detect mobile devices + */ + + $mobile_detect = new Mobile_Detect(); + $this->is_mobile = $mobile_detect->isMobile(); + $this->is_tablet = $mobile_detect->isTablet(); + + // Friendica-Client + $this->is_friendica_app = ($_SERVER['HTTP_USER_AGENT'] == "Apache-HttpClient/UNAVAILABLE (java 1.4)"); + + /* + * register template engines + */ + $dc = get_declared_classes(); + foreach ($dc as $k) { + if (in_array("ITemplateEngine", class_implements($k))){ + $this->register_template_engine($k); + } + } + + self::$a = $this; + + } + + public static function get_basepath() { + + $basepath = get_config("system", "basepath"); + + if ($basepath == "") + $basepath = dirname(__FILE__); + + if ($basepath == "") + $basepath = $_SERVER["DOCUMENT_ROOT"]; + + if ($basepath == "") + $basepath = $_SERVER["PWD"]; + + return($basepath); + } + + function get_scheme() { + return($this->scheme); + } + + /** + * @brief Retrieves the Friendica instance base URL + * + * This function assembles the base URL from multiple parts: + * - Protocol is determined either by the request or a combination of + * system.ssl_policy and the $ssl parameter. + * - Host name is determined either by system.hostname or inferred from request + * - Path is inferred from SCRIPT_NAME + * + * Note: $ssl parameter value doesn't directly correlate with the resulting protocol + * + * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN + * @return string Friendica server base URL + */ + function get_baseurl($ssl = false) { + + // Is the function called statically? + if (!(isset($this) && get_class($this) == __CLASS__)) { + return self::$a->get_baseurl($ssl); + } + + $scheme = $this->scheme; + + if (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 (Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) { + if ($ssl) { + $scheme = 'https'; + } else { + $scheme = 'http'; + } + } + + if (Config::get('config', 'hostname') != '') { + $this->hostname = Config::get('config', 'hostname'); + } + + return $scheme . "://" . $this->hostname . ((isset($this->path) && strlen($this->path)) ? '/' . $this->path : '' ); + } + + /** + * @brief Initializes the baseurl components + * + * Clears the baseurl cache to prevent inconstistencies + * + * @param string $url + */ + function set_baseurl($url) { + $parsed = @parse_url($url); + + if($parsed) { + $this->scheme = $parsed['scheme']; + + $hostname = $parsed['host']; + if (x($parsed, 'port')) { + $hostname .= ':' . $parsed['port']; + } + if (x($parsed, 'path')) { + $this->path = trim($parsed['path'], '\\/'); + } + + if (file_exists(".htpreconfig.php")) { + @include(".htpreconfig.php"); + } + + if (get_config('config', 'hostname') != '') { + $this->hostname = get_config('config', 'hostname'); + } + + if (!isset($this->hostname) OR ($this->hostname == '')) { + $this->hostname = $hostname; } - return false; } } -} -// called from db initialisation if db is dead. + function get_hostname() { + if (get_config('config','hostname') != "") + $this->hostname = get_config('config','hostname'); -if(! function_exists('system_unavailable')) { - function system_unavailable() { - include('system_unavailable.php'); - system_down(); - killme(); + return $this->hostname; + } + + function set_hostname($h) { + $this->hostname = $h; + } + + function set_path($p) { + $this->path = trim(trim($p),'/'); + } + + function get_path() { + return $this->path; + } + + function set_pager_total($n) { + $this->pager['total'] = intval($n); + } + + function set_pager_itemspage($n) { + $this->pager['itemspage'] = ((intval($n) > 0) ? intval($n) : 0); + $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; + } + + function set_pager_page($n) { + $this->pager['page'] = $n; + $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage']; + } + + function init_pagehead() { + $interval = ((local_user()) ? get_pconfig(local_user(),'system','update_interval') : 40000); + + // If the update is "deactivated" set it to the highest integer number (~24 days) + if ($interval < 0) + $interval = 2147483647; + + if($interval < 10000) + $interval = 40000; + + // compose the page title from the sitename and the + // current module called + if (!$this->module=='') + { + $this->page['title'] = $this->config['sitename'].' ('.$this->module.')'; + } else { + $this->page['title'] = $this->config['sitename']; + } + + /* put the head template at the beginning of page['htmlhead'] + * since the code added by the modules frequently depends on it + * being first + */ + if(!isset($this->page['htmlhead'])) + $this->page['htmlhead'] = ''; + + // If we're using Smarty, then doing replace_macros() will replace + // any unrecognized variables with a blank string. Since we delay + // replacing $stylesheet until later, we need to replace it now + // with another variable name + if($this->theme['template_engine'] === 'smarty3') + $stylesheet = $this->get_template_ldelim('smarty3') . '$stylesheet' . $this->get_template_rdelim('smarty3'); + else + $stylesheet = '$stylesheet'; + + $shortcut_icon = get_config("system", "shortcut_icon"); + if ($shortcut_icon == "") + $shortcut_icon = "images/friendica-32.png"; + + $touch_icon = get_config("system", "touch_icon"); + if ($touch_icon == "") + $touch_icon = "images/friendica-128.png"; + + // get data wich is needed for infinite scroll on the network page + $invinite_scroll = infinite_scroll_data($this->module); + + $tpl = get_markup_template('head.tpl'); + $this->page['htmlhead'] = replace_macros($tpl,array( + '$baseurl' => $this->get_baseurl(), // FIXME for z_path!!!! + '$local_user' => local_user(), + '$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION, + '$delitem' => t('Delete this item?'), + '$showmore' => t('show more'), + '$showfewer' => t('show fewer'), + '$update_interval' => $interval, + '$shortcut_icon' => $shortcut_icon, + '$touch_icon' => $touch_icon, + '$stylesheet' => $stylesheet, + '$infinite_scroll' => $invinite_scroll, + )) . $this->page['htmlhead']; + } + + function init_page_end() { + if(!isset($this->page['end'])) + $this->page['end'] = ''; + $tpl = get_markup_template('end.tpl'); + $this->page['end'] = replace_macros($tpl,array( + '$baseurl' => $this->get_baseurl() // FIXME for z_path!!!! + )) . $this->page['end']; + } + + function set_curl_code($code) { + $this->curl_code = $code; + } + + function get_curl_code() { + return $this->curl_code; + } + + function set_curl_content_type($content_type) { + $this->curl_content_type = $content_type; + } + + function get_curl_content_type() { + return $this->curl_content_type; + } + + function set_curl_headers($headers) { + $this->curl_headers = $headers; + } + + function get_curl_headers() { + return $this->curl_headers; + } + + function get_cached_avatar_image($avatar_image){ + return $avatar_image; + + // The following code is deactivated. It doesn't seem to make any sense and it slows down the system. + /* + if($this->cached_profile_image[$avatar_image]) + return $this->cached_profile_image[$avatar_image]; + + $path_parts = explode("/",$avatar_image); + $common_filename = $path_parts[count($path_parts)-1]; + + if($this->cached_profile_picdate[$common_filename]){ + $this->cached_profile_image[$avatar_image] = $avatar_image . $this->cached_profile_picdate[$common_filename]; + } else { + $r = q("SELECT `contact`.`avatar-date` AS picdate FROM `contact` WHERE `contact`.`thumb` like '%%/%s'", + $common_filename); + if (! dbm::is_result($r)) { + $this->cached_profile_image[$avatar_image] = $avatar_image; + } else { + $this->cached_profile_picdate[$common_filename] = "?rev=".urlencode($r[0]['picdate']); + $this->cached_profile_image[$avatar_image] = $avatar_image.$this->cached_profile_picdate[$common_filename]; + } + } + return $this->cached_profile_image[$avatar_image]; + */ + } + + + /** + * @brief Removes the baseurl from an url. This avoids some mixed content problems. + * + * @param string $orig_url + * + * @return string The cleaned url + */ + function remove_baseurl($orig_url){ + + // Is the function called statically? + if (!(isset($this) && get_class($this) == __CLASS__)) { + return(self::$a->remove_baseurl($orig_url)); + } + + // Remove the hostname from the url if it is an internal link + $nurl = normalise_link($orig_url); + $base = normalise_link($this->get_baseurl()); + $url = str_replace($base."/", "", $nurl); + + // if it is an external link return the orignal value + if ($url == normalise_link($orig_url)) { + return $orig_url; + } else { + return $url; + } + } + + /** + * @brief Register template engine class + * + * If $name is "", is used class static property $class::$name + * + * @param string $class + * @param string $name + */ + function register_template_engine($class, $name = '') { + if ($name===""){ + $v = get_class_vars( $class ); + if(x($v,"name")) $name = $v['name']; + } + if ($name===""){ + echo "template engine $class cannot be registered without a name.\n"; + killme(); + } + $this->template_engines[$name] = $class; + } + + /** + * @brief Return template engine instance. + * + * If $name is not defined, return engine defined by theme, + * or default + * + * @param strin $name Template engine name + * @return object Template Engine instance + */ + function template_engine($name = ''){ + if ($name!=="") { + $template_engine = $name; + } else { + $template_engine = 'smarty3'; + if (x($this->theme, 'template_engine')) { + $template_engine = $this->theme['template_engine']; + } + } + + if (isset($this->template_engines[$template_engine])){ + if(isset($this->template_engine_instance[$template_engine])){ + return $this->template_engine_instance[$template_engine]; + } else { + $class = $this->template_engines[$template_engine]; + $obj = new $class; + $this->template_engine_instance[$template_engine] = $obj; + return $obj; + } + } + + echo "template engine $template_engine is not registered!\n"; killme(); + } + + /** + * @brief Returns the active template engine. + * + * @return string + */ + function get_template_engine() { + return $this->theme['template_engine']; + } + + function set_template_engine($engine = 'smarty3') { + $this->theme['template_engine'] = $engine; + /* + $this->theme['template_engine'] = 'smarty3'; + + switch($engine) { + case 'smarty3': + if(is_writable('view/smarty3/')) + $this->theme['template_engine'] = 'smarty3'; + break; + default: + break; + } + */ + } + + function get_template_ldelim($engine = 'smarty3') { + return $this->ldelim[$engine]; + } + + function get_template_rdelim($engine = 'smarty3') { + return $this->rdelim[$engine]; + } + + function save_timestamp($stamp, $value) { + if (!isset($this->config['system']['profiler']) || !$this->config['system']['profiler']) + return; + + $duration = (float)(microtime(true)-$stamp); + + if (!isset($this->performance[$value])) { + // Prevent ugly E_NOTICE + $this->performance[$value] = 0; + } + + $this->performance[$value] += (float)$duration; + $this->performance["marktime"] += (float)$duration; + + $callstack = $this->callstack(); + + if (!isset($this->callstack[$value][$callstack])) { + // Prevent ugly E_NOTICE + $this->callstack[$value][$callstack] = 0; + } + + $this->callstack[$value][$callstack] += (float)$duration; + + } + + /** + * @brief Log active processes into the "process" table + */ + function start_process() { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1); + + $command = basename($trace[0]["file"]); + + $this->remove_inactive_processes(); + + q("START TRANSACTION"); + + $r = q("SELECT `pid` FROM `process` WHERE `pid` = %d", intval(getmypid())); + if (!dbm::is_result($r)) { + q("INSERT INTO `process` (`pid`,`command`,`created`) VALUES (%d, '%s', '%s')", + intval(getmypid()), + dbesc($command), + dbesc(datetime_convert())); + } + q("COMMIT"); + } + + /** + * @brief Remove inactive processes + */ + function remove_inactive_processes() { + q("START TRANSACTION"); + + $r = q("SELECT `pid` FROM `process`"); + if (dbm::is_result($r)) { + foreach ($r AS $process) { + if (!posix_kill($process["pid"], 0)) { + q("DELETE FROM `process` WHERE `pid` = %d", intval($process["pid"])); + } + } + } + q("COMMIT"); + } + + /** + * @brief Remove the active process from the "process" table + */ + function end_process() { + q("DELETE FROM `process` WHERE `pid` = %d", intval(getmypid())); + } + + /** + * @brief Returns a string with a callstack. Can be used for logging. + * + * @return string + */ + function callstack() { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 6); + + // We remove the first two items from the list since they contain data that we don't need. + array_shift($trace); + array_shift($trace); + + $callstack = array(); + foreach ($trace AS $func) + $callstack[] = $func["function"]; + + return implode(", ", $callstack); + } + + function get_useragent() { + return(FRIENDICA_PLATFORM." '".FRIENDICA_CODENAME."' ".FRIENDICA_VERSION."-".DB_UPDATE_VERSION."; ".$this->get_baseurl()); + } + + function is_friendica_app() { + return($this->is_friendica_app); + } + + /** + * @brief Checks if the site is called via a backend process + * + * This isn't a perfect solution. But we need this check very early. + * So we cannot wait until the modules are loaded. + * + * @return bool Is it a known backend? + */ + function is_backend() { + $backend = array(); + $backend[] = "_well_known"; + $backend[] = "api"; + $backend[] = "dfrn_notify"; + $backend[] = "fetch"; + $backend[] = "hcard"; + $backend[] = "hostxrd"; + $backend[] = "nodeinfo"; + $backend[] = "noscrape"; + $backend[] = "p"; + $backend[] = "poco"; + $backend[] = "post"; + $backend[] = "proxy"; + $backend[] = "pubsub"; + $backend[] = "pubsubhubbub"; + $backend[] = "receive"; + $backend[] = "rsd_xml"; + $backend[] = "salmon"; + $backend[] = "statistics_json"; + $backend[] = "xrd"; + + if (in_array($this->module, $backend)) + return(true); + else + return($this->backend); + } + + /** + * @brief Checks if the maximum number of database processes is reached + * + * @return bool Is the limit reached? + */ + function max_processes_reached() { + + if ($this->is_backend()) { + $process = "backend"; + $max_processes = get_config('system', 'max_processes_backend'); + if (intval($max_processes) == 0) + $max_processes = 5; + } else { + $process = "frontend"; + $max_processes = get_config('system', 'max_processes_frontend'); + if (intval($max_processes) == 0) + $max_processes = 20; + } + + $processlist = dbm::processlist(); + if ($processlist["list"] != "") { + logger("Processcheck: Processes: ".$processlist["amount"]." - Processlist: ".$processlist["list"], LOGGER_DEBUG); + + if ($processlist["amount"] > $max_processes) { + logger("Processcheck: Maximum number of processes for ".$process." tasks (".$max_processes.") reached.", LOGGER_DEBUG); + return true; + } + } + return false; + } + + /** + * @brief Checks if the maximum load is reached + * + * @return bool Is the load reached? + */ + function maxload_reached() { + + if ($this->is_backend()) { + $process = "backend"; + $maxsysload = intval(get_config('system', 'maxloadavg')); + if ($maxsysload < 1) + $maxsysload = 50; + } else { + $process = "frontend"; + $maxsysload = intval(get_config('system','maxloadavg_frontend')); + if ($maxsysload < 1) + $maxsysload = 50; + } + + $load = current_load(); + if ($load) { + if (intval($load) > $maxsysload) { + logger('system: load '.$load.' for '.$process.' tasks ('.$maxsysload.') too high.'); + return true; + } + } + return false; + } + + /** + * @brief Checks if the process is already running + * + * @param string $taskname The name of the task that will be used for the name of the lockfile + * @param string $task The path and name of the php script + * @param int $timeout The timeout after which a task should be killed + * + * @return bool Is the process running? + */ + function is_already_running($taskname, $task = "", $timeout = 540) { + + $lockpath = get_lockpath(); + if ($lockpath != '') { + $pidfile = new pidfile($lockpath, $taskname); + if ($pidfile->is_already_running()) { + logger("Already running"); + if ($pidfile->running_time() > $timeout) { + $pidfile->kill(); + logger("killed stale process"); + // Calling a new instance + if ($task != "") + proc_run(PRIORITY_MEDIUM, $task); + } + return true; + } + } + return false; + } + + function proc_run($args) { + + if (!function_exists("proc_open")) { + return; + } + + // Add the php path if it is a php call + if (count($args) && ($args[0] === 'php' OR !is_string($args[0]))) { + + // If the last worker fork was less than 10 seconds before then don't fork another one. + // This should prevent the forking of masses of workers. + if (get_config("system", "worker")) { + $cachekey = "app:proc_run:started"; + $result = Cache::get($cachekey); + if (!is_null($result)) { + if ((time() - $result) < 10) { + return; + } + } + // Set the timestamp of the last proc_run + Cache::set($cachekey, time(), CACHE_MINUTE); + } + + $args[0] = ((x($this->config,'php_path')) && (strlen($this->config['php_path'])) ? $this->config['php_path'] : 'php'); + } + + // add baseurl to args. cli scripts can't construct it + $args[] = $this->get_baseurl(); + + for($x = 0; $x < count($args); $x ++) + $args[$x] = escapeshellarg($args[$x]); + + $cmdline = implode($args," "); + + if(get_config('system','proc_windows')) + proc_close(proc_open('cmd /c start /b ' . $cmdline,array(),$foo,dirname(__FILE__))); + else + proc_close(proc_open($cmdline." &",array(),$foo,dirname(__FILE__))); + + } + + /** + * @brief Returns the system user that is executing the script + * + * This mostly returns something like "www-data". + * + * @return string system username + */ + static function systemuser() { + if (!function_exists('posix_getpwuid') OR !function_exists('posix_geteuid')) { + return ''; + } + + $processUser = posix_getpwuid(posix_geteuid()); + return $processUser['name']; + } + + /** + * @brief Checks if a given directory is usable for the system + * + * @return boolean the directory is usable + */ + static function directory_usable($directory) { + + if ($directory == '') { + logger("Directory is empty. This shouldn't happen.", LOGGER_DEBUG); + return false; + } + + if (!file_exists($directory)) { + logger('Path "'.$directory.'" does not exist for user '.self::systemuser(), LOGGER_DEBUG); + return false; + } + if (is_file($directory)) { + logger('Path "'.$directory.'" is a file for user '.self::systemuser(), LOGGER_DEBUG); + return false; + } + if (!is_dir($directory)) { + logger('Path "'.$directory.'" is not a directory for user '.self::systemuser(), LOGGER_DEBUG); + return false; + } + if (!is_writable($directory)) { + logger('Path "'.$directory.'" is not writable for user '.self::systemuser(), LOGGER_DEBUG); + return false; + } + return true; } } +/** + * @brief Retrieve the App structure + * + * Useful in functions which require it but don't get it passed to them + */ +function get_app() { + global $a; + return $a; +} + + +/** + * @brief Multi-purpose function to check variable state. + * + * Usage: x($var) or $x($array, 'key') + * + * returns false if variable/key is not set + * if variable is set, returns 1 if has 'non-zero' value, otherwise returns 0. + * e.g. x('') or x(0) returns 0; + * + * @param string|array $s variable to check + * @param string $k key inside the array to check + * + * @return bool|int + */ +function x($s,$k = NULL) { + if($k != NULL) { + if((is_array($s)) && (array_key_exists($k,$s))) { + if($s[$k]) + return (int) 1; + return (int) 0; + } + return false; + } + else { + if(isset($s)) { + if($s) { + return (int) 1; + } + return (int) 0; + } + return false; + } +} + + +/** + * @brief Called from db initialisation if db is dead. + */ +function system_unavailable() { + include('system_unavailable.php'); + system_down(); + killme(); +} function clean_urls() { - global $a; - // if($a->config['system']['clean_urls']) + $a = get_app(); return true; - // return false; } function z_path() { - global $a; - $base = $a->get_baseurl(); + $base = App::get_baseurl(); + if(! clean_urls()) $base .= '/?q='; + return $base; } +/** + * @brief Returns the baseurl. + * + * @see App::get_baseurl() + * + * @return string + * @TODO Maybe super-flous and deprecated? Seems to only wrap App::get_baseurl() + */ function z_root() { - global $a; - return $a->get_baseurl(); + return App::get_baseurl(); } +/** + * @brief Return absolut URL for given $path. + * + * @param string $path + * + * @return string + */ function absurl($path) { if(strpos($path,'/') === 0) return z_path() . $path; return $path; } +/** + * @brief Function to check if request was an AJAX (xmlhttprequest) request. + * + * @return boolean + */ function is_ajax() { return (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'); } @@ -1039,202 +1576,194 @@ function check_db() { $build = DB_UPDATE_VERSION; } if($build != DB_UPDATE_VERSION) - proc_run('php', 'include/dbupdate.php'); + proc_run(PRIORITY_CRITICAL, 'include/dbupdate.php'); } +/** + * Sets the base url for use in cmdline programs which don't have + * $_SERVER variables + */ +function check_url(App $a) { + $url = get_config('system','url'); -// Sets the base url for use in cmdline programs which don't have -// $_SERVER variables + // if the url isn't set or the stored url is radically different + // than the currently visited url, store the current value accordingly. + // "Radically different" ignores common variations such as http vs https + // and www.example.com vs example.com. + // We will only change the url to an ip address if there is no existing setting -if(! function_exists('check_url')) { - function check_url(&$a) { + if(! x($url)) + $url = set_config('system','url',App::get_baseurl()); + if((! link_compare($url,App::get_baseurl())) && (! preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/",$a->get_hostname))) + $url = set_config('system','url',App::get_baseurl()); - $url = get_config('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. - // "Radically different" ignores common variations such as http vs https - // and www.example.com vs example.com. - // We will only change the url to an ip address if there is no existing setting - - if(! x($url)) - $url = set_config('system','url',$a->get_baseurl()); - if((! link_compare($url,$a->get_baseurl())) && (! preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/",$a->get_hostname))) - $url = set_config('system','url',$a->get_baseurl()); - - return; - } + return; } -// Automatic database updates +/** + * @brief Automatic database updates + */ +function update_db(App $a) { + $build = get_config('system','build'); + if(! x($build)) + $build = set_config('system','build',DB_UPDATE_VERSION); -if(! function_exists('update_db')) { - function update_db(&$a) { - $build = get_config('system','build'); - if(! x($build)) - $build = set_config('system','build',DB_UPDATE_VERSION); + if($build != DB_UPDATE_VERSION) { + $stored = intval($build); + $current = intval(DB_UPDATE_VERSION); + if($stored < $current) { + Config::load('database'); - if($build != DB_UPDATE_VERSION) { - $stored = intval($build); - $current = intval(DB_UPDATE_VERSION); - if($stored < $current) { - load_config('database'); + // We're reporting a different version than what is currently installed. + // Run any existing update scripts to bring the database up to current. - // We're reporting a different version than what is currently installed. - // Run any existing update scripts to bring the database up to current. + // make sure that boot.php and update.php are the same release, we might be + // updating right this very second and the correct version of the update.php + // file may not be here yet. This can happen on a very busy site. - // make sure that boot.php and update.php are the same release, we might be - // updating right this very second and the correct version of the update.php - // file may not be here yet. This can happen on a very busy site. + if(DB_UPDATE_VERSION == UPDATE_VERSION) { + // Compare the current structure with the defined structure - if(DB_UPDATE_VERSION == UPDATE_VERSION) { - // Compare the current structure with the defined structure + $t = get_config('database','dbupdate_'.DB_UPDATE_VERSION); + if($t !== false) + return; - $t = get_config('database','dbupdate_'.DB_UPDATE_VERSION); - if($t !== false) - return; + set_config('database','dbupdate_'.DB_UPDATE_VERSION, time()); - set_config('database','dbupdate_'.DB_UPDATE_VERSION, time()); - - // run old update routine (wich could modify the schema and - // conflits with new routine) - for ($x = $stored; $x < NEW_UPDATE_ROUTINE_VERSION; $x++) { - $r = run_update_function($x); - if (!$r) break; - } - if ($stored < NEW_UPDATE_ROUTINE_VERSION) $stored = NEW_UPDATE_ROUTINE_VERSION; + // run old update routine (wich could modify the schema and + // conflits with new routine) + for ($x = $stored; $x < NEW_UPDATE_ROUTINE_VERSION; $x++) { + $r = run_update_function($x); + if (!$r) break; + } + if ($stored < NEW_UPDATE_ROUTINE_VERSION) $stored = NEW_UPDATE_ROUTINE_VERSION; - // run new update routine - // it update the structure in one call - $retval = update_structure(false, true); - if($retval) { - update_fail( - DB_UPDATE_VERSION, - $retval - ); - return; - } else { - set_config('database','dbupdate_'.DB_UPDATE_VERSION, 'success'); - } + // run new update routine + // it update the structure in one call + $retval = update_structure(false, true); + if($retval) { + update_fail( + DB_UPDATE_VERSION, + $retval + ); + return; + } else { + set_config('database','dbupdate_'.DB_UPDATE_VERSION, 'success'); + } - // run any left update_nnnn functions in update.php - for($x = $stored; $x < $current; $x ++) { - $r = run_update_function($x); - if (!$r) break; - } + // run any left update_nnnn functions in update.php + for($x = $stored; $x < $current; $x ++) { + $r = run_update_function($x); + if (!$r) break; } } } - - return; } + + return; } -if(!function_exists('run_update_function')){ - function run_update_function($x) { - if(function_exists('update_' . $x)) { - // There could be a lot of processes running or about to run. - // We want exactly one process to run the update command. - // So store the fact that we're taking responsibility - // after first checking to see if somebody else already has. +function run_update_function($x) { + if(function_exists('update_' . $x)) { - // If the update fails or times-out completely you may need to - // delete the config entry to try again. + // There could be a lot of processes running or about to run. + // We want exactly one process to run the update command. + // So store the fact that we're taking responsibility + // after first checking to see if somebody else already has. - $t = get_config('database','update_' . $x); - if($t !== false) - return false; - set_config('database','update_' . $x, time()); + // If the update fails or times-out completely you may need to + // delete the config entry to try again. - // call the specific update + $t = get_config('database','update_' . $x); + if($t !== false) + return false; + set_config('database','update_' . $x, time()); - $func = 'update_' . $x; - $retval = $func(); + // call the specific update - if($retval) { - //send the administrator an e-mail - update_fail( - $x, - sprintf(t('Update %s failed. See error logs.'), $x) - ); - return false; - } else { - set_config('database','update_' . $x, 'success'); - set_config('system','build', $x + 1); - return true; - } + $func = 'update_' . $x; + $retval = $func(); + + if($retval) { + //send the administrator an e-mail + update_fail( + $x, + sprintf(t('Update %s failed. See error logs.'), $x) + ); + return false; } else { set_config('database','update_' . $x, 'success'); set_config('system','build', $x + 1); return true; } + } else { + set_config('database','update_' . $x, 'success'); + set_config('system','build', $x + 1); return true; } + return true; } +/** + * @brief Synchronise plugins: + * + * $a->config['system']['addon'] contains a comma-separated list of names + * of plugins/addons which are used on this system. + * Go through the database list of already installed addons, and if we have + * an entry, but it isn't in the config list, call the uninstall procedure + * and mark it uninstalled in the database (for now we'll remove it). + * Then go through the config list and if we have a plugin that isn't installed, + * call the install procedure and add it to the database. + * + * @param App $a + * + */ +function check_plugins(App $a) { -if(! function_exists('check_plugins')) { - function check_plugins(&$a) { + $r = q("SELECT * FROM `addon` WHERE `installed` = 1"); + if (dbm::is_result($r)) + $installed = $r; + else + $installed = array(); - /** - * - * Synchronise plugins: - * - * $a->config['system']['addon'] contains a comma-separated list of names - * of plugins/addons which are used on this system. - * Go through the database list of already installed addons, and if we have - * an entry, but it isn't in the config list, call the uninstall procedure - * and mark it uninstalled in the database (for now we'll remove it). - * Then go through the config list and if we have a plugin that isn't installed, - * call the install procedure and add it to the database. - * - */ + $plugins = get_config('system','addon'); + $plugins_arr = array(); - $r = q("SELECT * FROM `addon` WHERE `installed` = 1"); - if(count($r)) - $installed = $r; - else - $installed = array(); + if($plugins) + $plugins_arr = explode(',',str_replace(' ', '',$plugins)); - $plugins = get_config('system','addon'); - $plugins_arr = array(); + $a->plugins = $plugins_arr; - if($plugins) - $plugins_arr = explode(',',str_replace(' ', '',$plugins)); + $installed_arr = array(); - $a->plugins = $plugins_arr; - - $installed_arr = array(); - - if(count($installed)) { - foreach($installed as $i) { - if(! in_array($i['name'],$plugins_arr)) { - uninstall_plugin($i['name']); - } - else { - $installed_arr[] = $i['name']; - } + if(count($installed)) { + foreach($installed as $i) { + if(! in_array($i['name'],$plugins_arr)) { + uninstall_plugin($i['name']); + } + else { + $installed_arr[] = $i['name']; } } - - if(count($plugins_arr)) { - foreach($plugins_arr as $p) { - if(! in_array($p,$installed_arr)) { - install_plugin($p); - } - } - } - - - load_hooks(); - - return; } + + if(count($plugins_arr)) { + foreach($plugins_arr as $p) { + if(! in_array($p,$installed_arr)) { + install_plugin($p); + } + } + } + + + load_hooks(); + + return; } function get_guid($size=16, $prefix = "") { @@ -1251,457 +1780,451 @@ function get_guid($size=16, $prefix = "") { $prefix = substr($prefix, 0, $size - 22); return(str_replace(".", "", uniqid($prefix, true))); } else { - $prefix = substr($prefix, 0, $size - 13); + $prefix = substr($prefix, 0, max($size - 13, 0)); return(uniqid($prefix)); } } -// wrapper for adding a login box. If $register == true provide a registration -// link. This will most always depend on the value of $a->config['register_policy']. -// returns the complete html for inserting into the page +/** + * @brief Wrapper for adding a login box. + * + * @param bool $register + * If $register == true provide a registration link. + * This will most always depend on the value of $a->config['register_policy']. + * @param bool $hiddens + * + * @return string + * Returns the complete html for inserting into the page + * + * @hooks 'login_hook' + * string $o + */ +function login($register = false, $hiddens=false) { + $a = get_app(); + $o = ""; + $reg = false; + if ($register) { + $reg = array( + 'title' => t('Create a New Account'), + 'desc' => t('Register') + ); + } -if(! function_exists('login')) { - function login($register = false, $hiddens=false) { - $a = get_app(); - $o = ""; - $reg = false; - if ($register) { - $reg = array( - 'title' => t('Create a New Account'), - 'desc' => t('Register') - ); - } + $noid = get_config('system','no_openid'); - $noid = get_config('system','no_openid'); - - $dest_url = $a->get_baseurl(true) . '/' . $a->query_string; - - if(local_user()) { - $tpl = get_markup_template("logout.tpl"); - } - else { - $a->page['htmlhead'] .= replace_macros(get_markup_template("login_head.tpl"),array( - '$baseurl' => $a->get_baseurl(true) - )); - - $tpl = get_markup_template("login.tpl"); - $_SESSION['return_url'] = $a->query_string; - $a->module = 'login'; - } - - $o .= replace_macros($tpl, array( - - '$dest_url' => $dest_url, - '$logout' => t('Logout'), - '$login' => t('Login'), - - '$lname' => array('username', t('Nickname or Email address: ') , '', ''), - '$lpassword' => array('password', t('Password: '), '', ''), - '$lremember' => array('remember', t('Remember me'), 0, ''), - - '$openid' => !$noid, - '$lopenid' => array('openid_url', t('Or login using OpenID: '),'',''), - - '$hiddens' => $hiddens, - - '$register' => $reg, - - '$lostpass' => t('Forgot your password?'), - '$lostlink' => t('Password Reset'), - - '$tostitle' => t('Website Terms of Service'), - '$toslink' => t('terms of service'), - - '$privacytitle' => t('Website Privacy Policy'), - '$privacylink' => t('privacy policy'), + $dest_url = $a->query_string; + if(local_user()) { + $tpl = get_markup_template("logout.tpl"); + } + else { + $a->page['htmlhead'] .= replace_macros(get_markup_template("login_head.tpl"),array( + '$baseurl' => $a->get_baseurl(true) )); - call_hooks('login_hook',$o); - - return $o; + $tpl = get_markup_template("login.tpl"); + $_SESSION['return_url'] = $a->query_string; + $a->module = 'login'; } -} -// Used to end the current process, after saving session state. + $o .= replace_macros($tpl, array( -if(! function_exists('killme')) { - function killme() { - session_write_close(); - exit; - } -} + '$dest_url' => $dest_url, + '$logout' => t('Logout'), + '$login' => t('Login'), -// redirect to another URL and terminate this process. + '$lname' => array('username', t('Nickname or Email: ') , '', ''), + '$lpassword' => array('password', t('Password: '), '', ''), + '$lremember' => array('remember', t('Remember me'), 0, ''), -if(! function_exists('goaway')) { - function goaway($s) { - header("Location: $s"); - killme(); - } -} + '$openid' => !$noid, + '$lopenid' => array('openid_url', t('Or login using OpenID: '),'',''), + '$hiddens' => $hiddens, -// Returns the uid of locally logged in user or false. + '$register' => $reg, -if(! function_exists('local_user')) { - function local_user() { - if((x($_SESSION,'authenticated')) && (x($_SESSION,'uid'))) - return intval($_SESSION['uid']); - return false; - } -} + '$lostpass' => t('Forgot your password?'), + '$lostlink' => t('Password Reset'), -// Returns contact id of authenticated site visitor or false + '$tostitle' => t('Website Terms of Service'), + '$toslink' => t('terms of service'), -if(! function_exists('remote_user')) { - function remote_user() { - if((x($_SESSION,'authenticated')) && (x($_SESSION,'visitor_id'))) - return intval($_SESSION['visitor_id']); - return false; - } -} + '$privacytitle' => t('Website Privacy Policy'), + '$privacylink' => t('privacy policy'), -// contents of $s are displayed prominently on the page the next time -// a page is loaded. Usually used for errors or alerts. + )); -if(! function_exists('notice')) { - /** - * Show an error message to user. - * - * This function save text in session, to be shown to the user at next page load - * - * @param string $s - Text of notice - */ - function notice($s) { - $a = get_app(); - if(! x($_SESSION,'sysmsg')) $_SESSION['sysmsg'] = array(); - if($a->interactive) - $_SESSION['sysmsg'][] = $s; - } -} -if(! function_exists('info')) { - /** - * Show an info message to user. - * - * This function save text in session, to be shown to the user at next page load - * - * @param string $s - Text of notice - */ - function info($s) { - $a = get_app(); + call_hooks('login_hook',$o); - if (local_user() AND get_pconfig(local_user(),'system','ignore_info')) - return; - - if(! x($_SESSION,'sysmsg_info')) $_SESSION['sysmsg_info'] = array(); - if($a->interactive) - $_SESSION['sysmsg_info'][] = $s; - } -} - - -// wrapper around config to limit the text length of an incoming message - -if(! function_exists('get_max_import_size')) { - function get_max_import_size() { - global $a; - return ((x($a->config,'max_import_size')) ? $a->config['max_import_size'] : 0 ); - } + return $o; } /** - * - * Wrap calls to proc_close(proc_open()) and call hook - * so plugins can take part in process :) - * - * args: - * $cmd program to run - * next args are passed as $cmd command line - * - * e.g.: proc_run("ls","-la","/tmp"); - * - * $cmd and string args are surrounded with "" + * @brief Used to end the current process, after saving session state. */ +function killme() { -if(! function_exists('proc_run')) { - function proc_run($cmd){ + if (!get_app()->is_backend()) + session_write_close(); - $a = get_app(); - - $args = func_get_args(); - - $newargs = array(); - if(! count($args)) - return; - - // expand any arrays - - foreach($args as $arg) { - if(is_array($arg)) { - foreach($arg as $n) { - $newargs[] = $n; - } - } - else - $newargs[] = $arg; - } - - $args = $newargs; - - $arr = array('args' => $args, 'run_cmd' => true); - - call_hooks("proc_run", $arr); - if(! $arr['run_cmd']) - return; - - if(count($args) && $args[0] === 'php') { - - if (get_config("system", "worker")) { - $argv = $args; - array_shift($argv); - - $parameters = json_encode($argv); - $found = q("SELECT `id` FROM `workerqueue` WHERE `parameter` = '%s'", - dbesc($parameters)); - - if (!$found) - q("INSERT INTO `workerqueue` (`parameter`, `created`, `priority`) - VALUES ('%s', '%s', %d)", - dbesc($parameters), - dbesc(datetime_convert()), - intval(0)); - - // Should we quit and wait for the poller to be called as a cronjob? - if (get_config("system", "worker_dont_fork")) - return; - - // Checking number of workers - $workers = q("SELECT COUNT(*) AS `workers` FROM `workerqueue` WHERE `executed` != '0000-00-00 00:00:00'"); - - // Get number of allowed number of worker threads - $queues = intval(get_config("system", "worker_queues")); - - if ($queues == 0) - $queues = 4; - - // If there are already enough workers running, don't fork another one - if ($workers[0]["workers"] >= $queues) - return; - - // Now call the poller to execute the jobs that we just added to the queue - $args = array("php", "include/poller.php", "no_cron"); - } - - $args[0] = ((x($a->config,'php_path')) && (strlen($a->config['php_path'])) ? $a->config['php_path'] : 'php'); - } - - // add baseurl to args. cli scripts can't construct it - $args[] = $a->get_baseurl(); - - for($x = 0; $x < count($args); $x ++) - $args[$x] = escapeshellarg($args[$x]); - - $cmdline = implode($args," "); - - if(get_config('system','proc_windows')) - proc_close(proc_open('cmd /c start /b ' . $cmdline,array(),$foo,dirname(__FILE__))); - else - proc_close(proc_open($cmdline." &",array(),$foo,dirname(__FILE__))); - } + exit; } -if(! function_exists('current_theme')) { - function current_theme(){ - $app_base_themes = array('duepuntozero', 'dispy', 'quattro'); +/** + * @brief Redirect to another URL and terminate this process. + */ +function goaway($s) { + if (!strstr(normalise_link($s), "http://")) + $s = App::get_baseurl()."/".$s; - $a = get_app(); + header("Location: $s"); + killme(); +} - $page_theme = null; - // Find the theme that belongs to the user whose stuff we are looking at +/** + * @brief Returns the user id of locally logged in user or false. + * + * @return int|bool user id or false + */ +function local_user() { + if((x($_SESSION,'authenticated')) && (x($_SESSION,'uid'))) + return intval($_SESSION['uid']); + return false; +} - if($a->profile_uid && ($a->profile_uid != local_user())) { - $r = q("select theme from user where uid = %d limit 1", - intval($a->profile_uid) - ); - if($r) - $page_theme = $r[0]['theme']; +/** + * @brief Returns contact id of authenticated site visitor or false + * + * @return int|bool visitor_id or false + */ +function remote_user() { + if((x($_SESSION,'authenticated')) && (x($_SESSION,'visitor_id'))) + return intval($_SESSION['visitor_id']); + return false; +} + +/** + * @brief Show an error message to user. + * + * This function save text in session, to be shown to the user at next page load + * + * @param string $s - Text of notice + */ +function notice($s) { + $a = get_app(); + if(! x($_SESSION,'sysmsg')) $_SESSION['sysmsg'] = array(); + if($a->interactive) + $_SESSION['sysmsg'][] = $s; +} + +/** + * @brief Show an info message to user. + * + * This function save text in session, to be shown to the user at next page load + * + * @param string $s - Text of notice + */ +function info($s) { + $a = get_app(); + + if (local_user() AND get_pconfig(local_user(),'system','ignore_info')) + return; + + if(! x($_SESSION,'sysmsg_info')) $_SESSION['sysmsg_info'] = array(); + if($a->interactive) + $_SESSION['sysmsg_info'][] = $s; +} + + +/** + * @brief Wrapper around config to limit the text length of an incoming message + * + * @return int + */ +function get_max_import_size() { + $a = get_app(); + return ((x($a->config,'max_import_size')) ? $a->config['max_import_size'] : 0 ); +} + +/** + * @brief Wrap calls to proc_close(proc_open()) and call hook + * so plugins can take part in process :) + * + * @param (string|integer|array) $cmd program to run, priority or parameter array + * + * next args are passed as $cmd command line + * e.g.: proc_run("ls","-la","/tmp"); + * or: proc_run(PRIORITY_HIGH, "include/notifier.php", "drop", $drop_id); + * or: proc_run(array('priority' => PRIORITY_HIGH, 'dont_fork' => true), "include/create_shadowentry.php", $post_id); + * + * @note $cmd and string args are surrounded with "" + * + * @hooks 'proc_run' + * array $arr + */ +function proc_run($cmd){ + + $a = get_app(); + + $proc_args = func_get_args(); + + $args = array(); + if (!count($proc_args)) { + return; + } + + // Preserve the first parameter + // It could contain a command, the priority or an parameter array + // If we use the parameter array we have to protect it from the following function + $run_parameter = array_shift($proc_args); + + // expand any arrays + foreach ($proc_args as $arg) { + if (is_array($arg)) { + foreach ($arg as $n) { + $args[] = $n; + } + } else { + $args[] = $arg; } + } - // Allow folks to over-rule user themes and always use their own on their own site. - // This works only if the user is on the same server + // Now we add the run parameters back to the array + array_unshift($args, $run_parameter); - if($page_theme && local_user() && (local_user() != $a->profile_uid)) { - if(get_pconfig(local_user(),'system','always_my_theme')) - $page_theme = null; + $arr = array('args' => $args, 'run_cmd' => true); + + call_hooks("proc_run", $arr); + if (!$arr['run_cmd'] OR !count($args)) + return; + + if (!get_config("system", "worker") OR (is_string($run_parameter) AND ($run_parameter != 'php'))) { + $a->proc_run($args); + return; + } + + $priority = PRIORITY_MEDIUM; + $dont_fork = get_config("system", "worker_dont_fork"); + + if (is_int($run_parameter)) { + $priority = $run_parameter; + } elseif (is_array($run_parameter)) { + if (isset($run_parameter['priority'])) { + $priority = $run_parameter['priority']; } + if (isset($run_parameter['dont_fork'])) { + $dont_fork = $run_parameter['dont_fork']; + } + } + + $argv = $args; + array_shift($argv); + + $parameters = json_encode($argv); + $found = q("SELECT `id` FROM `workerqueue` WHERE `parameter` = '%s'", + dbesc($parameters)); + + if (!$found) + q("INSERT INTO `workerqueue` (`parameter`, `created`, `priority`) + VALUES ('%s', '%s', %d)", + dbesc($parameters), + dbesc(datetime_convert()), + intval($priority)); + + // Should we quit and wait for the poller to be called as a cronjob? + if ($dont_fork) { + return; + } + + // Checking number of workers + $workers = q("SELECT COUNT(*) AS `workers` FROM `workerqueue` WHERE `executed` != '0000-00-00 00:00:00'"); + + // Get number of allowed number of worker threads + $queues = intval(get_config("system", "worker_queues")); + + if ($queues == 0) + $queues = 4; + + // If there are already enough workers running, don't fork another one + if ($workers[0]["workers"] >= $queues) + return; + + // Now call the poller to execute the jobs that we just added to the queue + $args = array("php", "include/poller.php", "no_cron"); + + $a->proc_run($args); +} + +function current_theme(){ + $app_base_themes = array('duepuntozero', 'dispy', 'quattro'); + + $a = get_app(); + + $page_theme = null; + + // Find the theme that belongs to the user whose stuff we are looking at + + if($a->profile_uid && ($a->profile_uid != local_user())) { + $r = q("select theme from user where uid = %d limit 1", + intval($a->profile_uid) + ); + if (dbm::is_result($r)) + $page_theme = $r[0]['theme']; + } + + // Allow folks to over-rule user themes and always use their own on their own site. + // This works only if the user is on the same server + + if($page_theme && local_user() && (local_user() != $a->profile_uid)) { + if(get_pconfig(local_user(),'system','always_my_theme')) + $page_theme = null; + } // $mobile_detect = new Mobile_Detect(); // $is_mobile = $mobile_detect->isMobile() || $mobile_detect->isTablet(); - $is_mobile = $a->is_mobile || $a->is_tablet; + $is_mobile = $a->is_mobile || $a->is_tablet; - $standard_system_theme = ((isset($a->config['system']['theme'])) ? $a->config['system']['theme'] : ''); - $standard_theme_name = ((isset($_SESSION) && x($_SESSION,'theme')) ? $_SESSION['theme'] : $standard_system_theme); + $standard_system_theme = Config::get('system', 'theme', ''); + $standard_theme_name = ((isset($_SESSION) && x($_SESSION,'theme')) ? $_SESSION['theme'] : $standard_system_theme); - if($is_mobile) { - if(isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) { - $system_theme = $standard_system_theme; - $theme_name = $standard_theme_name; - } - else { - $system_theme = ((isset($a->config['system']['mobile-theme'])) ? $a->config['system']['mobile-theme'] : $standard_system_theme); - $theme_name = ((isset($_SESSION) && x($_SESSION,'mobile-theme')) ? $_SESSION['mobile-theme'] : $system_theme); - - if($theme_name === '---') { - // user has selected to have the mobile theme be the same as the normal one - $system_theme = $standard_system_theme; - $theme_name = $standard_theme_name; - - if($page_theme) - $theme_name = $page_theme; - } - } - } - else { + if ($is_mobile) { + if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) { $system_theme = $standard_system_theme; $theme_name = $standard_theme_name; + } else { + $system_theme = Config::get('system', 'mobile-theme', ''); + if ($system_theme == '') { + $system_theme = $standard_system_theme; + } + $theme_name = ((isset($_SESSION) && x($_SESSION,'mobile-theme')) ? $_SESSION['mobile-theme'] : $system_theme); - if($page_theme) - $theme_name = $page_theme; - } + if($theme_name === '---') { + // user has selected to have the mobile theme be the same as the normal one + $system_theme = $standard_system_theme; + $theme_name = $standard_theme_name; - if($theme_name && - (file_exists('view/theme/' . $theme_name . '/style.css') || - file_exists('view/theme/' . $theme_name . '/style.php'))) - return($theme_name); - - foreach($app_base_themes as $t) { - if(file_exists('view/theme/' . $t . '/style.css')|| - file_exists('view/theme/' . $t . '/style.php')) - return($t); - } - - $fallback = array_merge(glob('view/theme/*/style.css'),glob('view/theme/*/style.php')); - if(count($fallback)) - return (str_replace('view/theme/','', substr($fallback[0],0,-10))); - - } -} - -/* - * Return full URL to theme which is currently in effect. -* Provide a sane default if nothing is chosen or the specified theme does not exist. -*/ -if(! function_exists('current_theme_url')) { - function current_theme_url() { - global $a; - - $t = current_theme(); - - $opts = (($a->profile_uid) ? '?f=&puid=' . $a->profile_uid : ''); - if (file_exists('view/theme/' . $t . '/style.php')) - return($a->get_baseurl() . '/view/theme/' . $t . '/style.pcss' . $opts); - - return($a->get_baseurl() . '/view/theme/' . $t . '/style.css'); - } -} - -if(! function_exists('feed_birthday')) { - function feed_birthday($uid,$tz) { - - /** - * - * Determine the next birthday, but only if the birthday is published - * in the default profile. We _could_ also look for a private profile that the - * recipient can see, but somebody could get mad at us if they start getting - * public birthday greetings when they haven't made this info public. - * - * Assuming we are able to publish this info, we are then going to convert - * the start time from the owner's timezone to UTC. - * - * This will potentially solve the problem found with some social networks - * where birthdays are converted to the viewer's timezone and salutations from - * elsewhere in the world show up on the wrong day. We will convert it to the - * viewer's timezone also, but first we are going to convert it from the birthday - * person's timezone to GMT - so the viewer may find the birthday starting at - * 6:00PM the day before, but that will correspond to midnight to the birthday person. - * - */ - - - $birthday = ''; - - if(! strlen($tz)) - $tz = 'UTC'; - - $p = q("SELECT `dob` FROM `profile` WHERE `is-default` = 1 AND `uid` = %d LIMIT 1", - intval($uid) - ); - - if($p && count($p)) { - $tmp_dob = substr($p[0]['dob'],5); - if(intval($tmp_dob)) { - $y = datetime_convert($tz,$tz,'now','Y'); - $bd = $y . '-' . $tmp_dob . ' 00:00'; - $t_dob = strtotime($bd); - $now = strtotime(datetime_convert($tz,$tz,'now')); - if($t_dob < $now) - $bd = $y + 1 . '-' . $tmp_dob . ' 00:00'; - $birthday = datetime_convert($tz,'UTC',$bd,ATOM_TIME); + if($page_theme) + $theme_name = $page_theme; } } - - return $birthday; } -} + else { + $system_theme = $standard_system_theme; + $theme_name = $standard_theme_name; -if(! function_exists('is_site_admin')) { - function is_site_admin() { - $a = get_app(); - - $adminlist = explode(",", str_replace(" ", "", $a->config['admin_email'])); - - //if(local_user() && x($a->user,'email') && x($a->config,'admin_email') && ($a->user['email'] === $a->config['admin_email'])) - if(local_user() && x($a->user,'email') && x($a->config,'admin_email') && in_array($a->user['email'], $adminlist)) - return true; - return false; + if($page_theme) + $theme_name = $page_theme; } -} + if($theme_name && + (file_exists('view/theme/' . $theme_name . '/style.css') || + file_exists('view/theme/' . $theme_name . '/style.php'))) + return($theme_name); -if(! function_exists('load_contact_links')) { - function load_contact_links($uid) { - - $a = get_app(); - - $ret = array(); - - if(! $uid || x($a->contacts,'empty')) - return; - - $r = q("SELECT `id`,`network`,`url`,`thumb`, `rel` FROM `contact` WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `thumb` != ''", - intval($uid) - ); - if(count($r)) { - foreach($r as $rr){ - $url = normalise_link($rr['url']); - $ret[$url] = $rr; - } - } else - $ret['empty'] = true; - - $a->contacts = $ret; - return; + foreach($app_base_themes as $t) { + if(file_exists('view/theme/' . $t . '/style.css')|| + file_exists('view/theme/' . $t . '/style.php')) + return($t); } + + $fallback = array_merge(glob('view/theme/*/style.css'),glob('view/theme/*/style.php')); + if(count($fallback)) + return (str_replace('view/theme/','', substr($fallback[0],0,-10))); + } /** -* returns querystring as string from a mapped array -* -* @param params Array -* @return string -*/ + * @brief Return full URL to theme which is currently in effect. + * + * Provide a sane default if nothing is chosen or the specified theme does not exist. + * + * @return string + */ +function current_theme_url() { + $a = get_app(); + + $t = current_theme(); + + $opts = (($a->profile_uid) ? '?f=&puid=' . $a->profile_uid : ''); + if (file_exists('view/theme/' . $t . '/style.php')) + return('view/theme/'.$t.'/style.pcss'.$opts); + + return('view/theme/'.$t.'/style.css'); +} + +function feed_birthday($uid,$tz) { + + /** + * + * Determine the next birthday, but only if the birthday is published + * in the default profile. We _could_ also look for a private profile that the + * recipient can see, but somebody could get mad at us if they start getting + * public birthday greetings when they haven't made this info public. + * + * Assuming we are able to publish this info, we are then going to convert + * the start time from the owner's timezone to UTC. + * + * This will potentially solve the problem found with some social networks + * where birthdays are converted to the viewer's timezone and salutations from + * elsewhere in the world show up on the wrong day. We will convert it to the + * viewer's timezone also, but first we are going to convert it from the birthday + * person's timezone to GMT - so the viewer may find the birthday starting at + * 6:00PM the day before, but that will correspond to midnight to the birthday person. + * + */ + + + $birthday = ''; + + if(! strlen($tz)) + $tz = 'UTC'; + + $p = q("SELECT `dob` FROM `profile` WHERE `is-default` = 1 AND `uid` = %d LIMIT 1", + intval($uid) + ); + + if (dbm::is_result($p)) { + $tmp_dob = substr($p[0]['dob'],5); + if(intval($tmp_dob)) { + $y = datetime_convert($tz,$tz,'now','Y'); + $bd = $y . '-' . $tmp_dob . ' 00:00'; + $t_dob = strtotime($bd); + $now = strtotime(datetime_convert($tz,$tz,'now')); + if($t_dob < $now) + $bd = $y + 1 . '-' . $tmp_dob . ' 00:00'; + $birthday = datetime_convert($tz,'UTC',$bd,ATOM_TIME); + } + } + + return $birthday; +} + +/** + * @brief Check if current user has admin role. + * + * @return bool true if user is an admin + */ +function is_site_admin() { + $a = get_app(); + + $adminlist = explode(",", str_replace(" ", "", $a->config['admin_email'])); + + //if(local_user() && x($a->user,'email') && x($a->config,'admin_email') && ($a->user['email'] === $a->config['admin_email'])) + if(local_user() && x($a->user,'email') && x($a->config,'admin_email') && in_array($a->user['email'], $adminlist)) + return true; + return false; +} + +/** + * @brief Returns querystring as string from a mapped array. + * + * @param array $params mapped array with query parameters + * @param string $name of parameter, default null + * + * @return string + */ function build_querystring($params, $name=null) { $ret = ""; foreach($params as $key=>$val) { @@ -1843,8 +2366,9 @@ function get_itemcachepath() { return ""; $itemcache = get_config('system','itemcache'); - if (($itemcache != "") AND is_dir($itemcache) AND is_writable($itemcache)) - return($itemcache); + if (($itemcache != "") AND App::directory_usable($itemcache)) { + return $itemcache; + } $temppath = get_temppath(); @@ -1854,9 +2378,9 @@ function get_itemcachepath() { mkdir($itemcache); } - if (is_dir($itemcache) AND is_writable($itemcache)) { + if (App::directory_usable($itemcache)) { set_config("system", "itemcache", $itemcache); - return($itemcache); + return $itemcache; } } return ""; @@ -1864,52 +2388,112 @@ function get_itemcachepath() { function get_lockpath() { $lockpath = get_config('system','lockpath'); - if (($lockpath != "") AND is_dir($lockpath) AND is_writable($lockpath)) - return($lockpath); + if (($lockpath != "") AND App::directory_usable($lockpath)) { + // We have a lock path and it is usable + return $lockpath; + } + // We don't have a working preconfigured lock path, so we take the temp path. $temppath = get_temppath(); if ($temppath != "") { + // To avoid any interferences with other systems we create our own directory $lockpath = $temppath."/lock"; - - if (!is_dir($lockpath)) + if (!is_dir($lockpath)) { mkdir($lockpath); - elseif (!is_writable($lockpath)) - $lockpath = $temppath; + } - if (is_dir($lockpath) AND is_writable($lockpath)) { + if (App::directory_usable($lockpath)) { + // The new path is usable, we are happy set_config("system", "lockpath", $lockpath); - return($lockpath); + return $lockpath; + } else { + // We can't create a subdirectory, strange. + // But the directory seems to work, so we use it but don't store it. + return $temppath; } } + + // Reaching this point means that the operating system is configured badly. + return ""; +} + +/** + * @brief Returns the path where spool files are stored + * + * @return string Spool path + */ +function get_spoolpath() { + $spoolpath = get_config('system','spoolpath'); + if (($spoolpath != "") AND App::directory_usable($spoolpath)) { + // We have a spool path and it is usable + return $spoolpath; + } + + // We don't have a working preconfigured spool path, so we take the temp path. + $temppath = get_temppath(); + + if ($temppath != "") { + // To avoid any interferences with other systems we create our own directory + $spoolpath = $temppath."/spool"; + if (!is_dir($spoolpath)) { + mkdir($spoolpath); + } + + if (App::directory_usable($spoolpath)) { + // The new path is usable, we are happy + set_config("system", "spoolpath", $spoolpath); + return $spoolpath; + } else { + // We can't create a subdirectory, strange. + // But the directory seems to work, so we use it but don't store it. + return $temppath; + } + } + + // Reaching this point means that the operating system is configured badly. return ""; } function get_temppath() { $a = get_app(); - $temppath = get_config("system","temppath"); - if (($temppath != "") AND is_dir($temppath) AND is_writable($temppath)) - return($temppath); + $temppath = get_config("system", "temppath"); + if (($temppath != "") AND App::directory_usable($temppath)) { + // We have a temp path and it is usable + return $temppath; + } + + // We don't have a working preconfigured temp path, so we take the system path. $temppath = sys_get_temp_dir(); - if (($temppath != "") AND is_dir($temppath) AND is_writable($temppath)) { - $temppath .= "/".$a->get_hostname(); - if (!is_dir($temppath)) - mkdir($temppath); - if (is_dir($temppath) AND is_writable($temppath)) { - set_config("system", "temppath", $temppath); - return($temppath); + // Check if it is usable + if (($temppath != "") AND App::directory_usable($temppath)) { + // To avoid any interferences with other systems we create our own directory + $new_temppath .= "/".$a->get_hostname(); + if (!is_dir($new_temppath)) + mkdir($new_temppath); + + if (App::directory_usable($new_temppath)) { + // The new path is usable, we are happy + set_config("system", "temppath", $new_temppath); + return $new_temppath; + } else { + // We can't create a subdirectory, strange. + // But the directory seems to work, so we use it but don't store it. + return $temppath; } } - return(""); + // Reaching this point means that the operating system is configured badly. + return ''; } -function set_template_engine(&$a, $engine = 'internal') { -// This function is no longer necessary, but keep it as a wrapper to the class method -// to avoid breaking themes again unnecessarily +/// @deprecated +function set_template_engine(App $a, $engine = 'internal') { +/// @note This function is no longer necessary, but keep it as a wrapper to the class method +/// to avoid breaking themes again unnecessarily $a->set_template_engine($engine); } @@ -1958,5 +2542,67 @@ function current_load() { if (!is_array($load_arr)) return false; - return max($load_arr); + return max($load_arr[0], $load_arr[1]); +} + +/** + * @brief get c-style args + * + * @return int + */ +function argc() { + return get_app()->argc; +} + +/** + * @brief Returns the value of a argv key + * + * @param int $x argv key + * @return string Value of the argv key + */ +function argv($x) { + if(array_key_exists($x,get_app()->argv)) + return get_app()->argv[$x]; + + return ''; +} + +/** + * @brief Get the data which is needed for infinite scroll + * + * For invinite scroll we need the page number of the actual page + * and the the URI where the content of the next page comes from. + * This data is needed for the js part in main.js. + * Note: infinite scroll does only work for the network page (module) + * + * @param string $module The name of the module (e.g. "network") + * @return array Of infinite scroll data + * 'pageno' => $pageno The number of the actual page + * 'reload_uri' => $reload_uri The URI of the content we have to load + */ +function infinite_scroll_data($module) { + + if (get_pconfig(local_user(),'system','infinite_scroll') + AND ($module == "network") AND ($_GET["mode"] != "minimal")) { + + // get the page number + if (is_string($_GET["page"])) + $pageno = $_GET["page"]; + else + $pageno = 1; + + $reload_uri = ""; + + // try to get the uri from which we load the content + foreach ($_GET AS $param => $value) + if (($param != "page") AND ($param != "q")) + $reload_uri .= "&".$param."=".urlencode($value); + + if (($a->page_offset != "") AND !strstr($reload_uri, "&offset=")) + $reload_uri .= "&offset=".urlencode($a->page_offset); + + $arr = array("pageno" => $pageno, "reload_uri" => $reload_uri); + + return $arr; + } } diff --git a/convert_innodb.sql b/convert_innodb.sql deleted file mode 100644 index 9eeb67fe85..0000000000 --- a/convert_innodb.sql +++ /dev/null @@ -1,19 +0,0 @@ - - -ALTER TABLE `profile` DROP INDEX `pub_keywords` ; -ALTER TABLE `profile` DROP INDEX `prv_keywords` ; - -ALTER TABLE `item` DROP INDEX `title` ; -ALTER TABLE `item` DROP INDEX `body` ; -ALTER TABLE `item` DROP INDEX `allow_cid` ; -ALTER TABLE `item` DROP INDEX `allow_gid` ; -ALTER TABLE `item` DROP INDEX `deny_cid` ; -ALTER TABLE `item` DROP INDEX `deny_gid` ; -ALTER TABLE `item` DROP INDEX `tag` ; -ALTER TABLE `item` DROP INDEX `file` ; - - -SELECT CONCAT('ALTER TABLE ',table_schema,'.',table_name,' engine=InnoDB;') -FROM information_schema.tables -WHERE engine = 'MyISAM'; - diff --git a/database.sql b/database.sql index e3768c1efb..b133489e4e 100644 --- a/database.sql +++ b/database.sql @@ -1,6 +1,6 @@ -- ------------------------------------------ --- Friendica 3.4.2 (Lily of the valley) --- DB_UPDATE_VERSION 1190 +-- Friendica 3.5.1-rc (Asparagus) +-- DB_UPDATE_VERSION 1215 -- ------------------------------------------ @@ -8,20 +8,22 @@ -- TABLE addon -- CREATE TABLE IF NOT EXISTS `addon` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, - `name` varchar(255) NOT NULL DEFAULT '', + `id` int(11) NOT NULL auto_increment, + `name` varchar(190) NOT NULL DEFAULT '', `version` varchar(255) NOT NULL DEFAULT '', `installed` tinyint(1) NOT NULL DEFAULT 0, `hidden` tinyint(1) NOT NULL DEFAULT 0, `timestamp` bigint(20) NOT NULL DEFAULT 0, - `plugin_admin` tinyint(1) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `plugin_admin` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), + UNIQUE INDEX `name` (`name`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE attach -- CREATE TABLE IF NOT EXISTS `attach` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `hash` varchar(64) NOT NULL DEFAULT '', `filename` varchar(255) NOT NULL DEFAULT '', @@ -30,74 +32,80 @@ CREATE TABLE IF NOT EXISTS `attach` ( `data` longblob NOT NULL, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `allow_cid` mediumtext NOT NULL, - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL -) DEFAULT CHARSET=utf8; + `allow_cid` mediumtext, + `allow_gid` mediumtext, + `deny_cid` mediumtext, + `deny_gid` mediumtext, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE auth_codes -- CREATE TABLE IF NOT EXISTS `auth_codes` ( - `id` varchar(40) NOT NULL PRIMARY KEY, + `id` varchar(40) NOT NULL, `client_id` varchar(20) NOT NULL DEFAULT '', `redirect_uri` varchar(200) NOT NULL DEFAULT '', `expires` int(11) NOT NULL DEFAULT 0, - `scope` varchar(250) NOT NULL DEFAULT '' -) DEFAULT CHARSET=utf8; + `scope` varchar(250) NOT NULL DEFAULT '', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE cache -- CREATE TABLE IF NOT EXISTS `cache` ( - `k` varchar(255) NOT NULL PRIMARY KEY, - `v` text NOT NULL, + `k` varbinary(255) NOT NULL, + `v` mediumtext, `expire_mode` int(11) NOT NULL DEFAULT 0, `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - INDEX `updated` (`updated`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`k`), + INDEX `expire_mode_updated` (`expire_mode`,`updated`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE challenge -- CREATE TABLE IF NOT EXISTS `challenge` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `challenge` varchar(255) NOT NULL DEFAULT '', `dfrn-id` varchar(255) NOT NULL DEFAULT '', `expire` int(11) NOT NULL DEFAULT 0, `type` varchar(255) NOT NULL DEFAULT '', - `last_update` varchar(255) NOT NULL DEFAULT '' -) DEFAULT CHARSET=utf8; + `last_update` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE clients -- CREATE TABLE IF NOT EXISTS `clients` ( - `client_id` varchar(20) NOT NULL PRIMARY KEY, + `client_id` varchar(20) NOT NULL, `pw` varchar(20) NOT NULL DEFAULT '', `redirect_uri` varchar(200) NOT NULL DEFAULT '', `name` text, `icon` text, - `uid` int(11) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `uid` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`client_id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE config -- CREATE TABLE IF NOT EXISTS `config` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, - `cat` varchar(255) NOT NULL DEFAULT '', - `k` varchar(255) NOT NULL DEFAULT '', - `v` text NOT NULL, - INDEX `cat_k` (`cat`(30),`k`(30)) -) DEFAULT CHARSET=utf8; + `id` int(10) unsigned NOT NULL auto_increment, + `cat` varbinary(255) NOT NULL DEFAULT '', + `k` varbinary(255) NOT NULL DEFAULT '', + `v` mediumtext, + PRIMARY KEY(`id`), + UNIQUE INDEX `cat_k` (`cat`,`k`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE contact -- CREATE TABLE IF NOT EXISTS `contact` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `self` tinyint(1) NOT NULL DEFAULT 0, @@ -108,28 +116,30 @@ CREATE TABLE IF NOT EXISTS `contact` ( `name` varchar(255) NOT NULL DEFAULT '', `nick` varchar(255) NOT NULL DEFAULT '', `location` varchar(255) NOT NULL DEFAULT '', - `about` text NOT NULL, - `keywords` text NOT NULL, + `about` text, + `keywords` text, `gender` varchar(32) NOT NULL DEFAULT '', + `xmpp` varchar(255) NOT NULL DEFAULT '', `attag` varchar(255) NOT NULL DEFAULT '', - `photo` text NOT NULL, - `thumb` text NOT NULL, - `micro` text NOT NULL, - `site-pubkey` text NOT NULL, + `avatar` varchar(255) NOT NULL DEFAULT '', + `photo` text, + `thumb` text, + `micro` text, + `site-pubkey` text, `issued-id` varchar(255) NOT NULL DEFAULT '', `dfrn-id` varchar(255) NOT NULL DEFAULT '', `url` varchar(255) NOT NULL DEFAULT '', `nurl` varchar(255) NOT NULL DEFAULT '', `addr` varchar(255) NOT NULL DEFAULT '', `alias` varchar(255) NOT NULL DEFAULT '', - `pubkey` text NOT NULL, - `prvkey` text NOT NULL, + `pubkey` text, + `prvkey` text, `batch` varchar(255) NOT NULL DEFAULT '', - `request` text NOT NULL, - `notify` text NOT NULL, - `poll` text NOT NULL, - `confirm` text NOT NULL, - `poco` text NOT NULL, + `request` text, + `notify` text, + `poll` text, + `confirm` text, + `poco` text, `aes_allow` tinyint(1) NOT NULL DEFAULT 0, `ret-aes` tinyint(1) NOT NULL DEFAULT 0, `usehub` tinyint(1) NOT NULL DEFAULT 0, @@ -149,62 +159,69 @@ CREATE TABLE IF NOT EXISTS `contact` ( `writable` tinyint(1) NOT NULL DEFAULT 0, `forum` tinyint(1) NOT NULL DEFAULT 0, `prv` tinyint(1) NOT NULL DEFAULT 0, + `contact-type` int(11) unsigned NOT NULL DEFAULT 0, `hidden` tinyint(1) NOT NULL DEFAULT 0, `archive` tinyint(1) NOT NULL DEFAULT 0, `pending` tinyint(1) NOT NULL DEFAULT 1, `rating` tinyint(1) NOT NULL DEFAULT 0, - `reason` text NOT NULL, + `reason` text, `closeness` tinyint(2) NOT NULL DEFAULT 99, - `info` mediumtext NOT NULL, + `info` mediumtext, `profile-id` int(11) NOT NULL DEFAULT 0, `bdyear` varchar(4) NOT NULL DEFAULT '', `bd` date NOT NULL DEFAULT '0000-00-00', `notify_new_posts` tinyint(1) NOT NULL DEFAULT 0, `fetch_further_information` tinyint(1) NOT NULL DEFAULT 0, - `ffi_keyword_blacklist` mediumtext NOT NULL, - INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; + `ffi_keyword_blacklist` text, + PRIMARY KEY(`id`), + INDEX `uid_name` (`uid`,`name`), + INDEX `self_uid` (`self`,`uid`), + INDEX `alias_uid` (`alias`(32),`uid`), + INDEX `pending_uid` (`pending`,`uid`), + INDEX `blocked_uid` (`blocked`,`uid`), + INDEX `uid_rel_network_poll` (`uid`,`rel`,`network`,`poll`(64),`archive`), + INDEX `uid_network_batch` (`uid`,`network`,`batch`(64)), + INDEX `addr_uid` (`addr`(32),`uid`), + INDEX `nurl_uid` (`nurl`(32),`uid`), + INDEX `nick_uid` (`nick`(32),`uid`), + INDEX `dfrn-id` (`dfrn-id`), + INDEX `issued-id` (`issued-id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE conv -- CREATE TABLE IF NOT EXISTS `conv` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `guid` varchar(64) NOT NULL DEFAULT '', - `recips` mediumtext NOT NULL, + `recips` text, `uid` int(11) NOT NULL DEFAULT 0, `creator` varchar(255) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `subject` mediumtext NOT NULL, + `subject` text, + PRIMARY KEY(`id`), INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE deliverq -- CREATE TABLE IF NOT EXISTS `deliverq` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, - `cmd` varchar(32) NOT NULL DEFAULT '', + `id` int(10) unsigned NOT NULL auto_increment, + `cmd` varbinary(32) NOT NULL DEFAULT '', `item` int(11) NOT NULL DEFAULT 0, - `contact` int(11) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; - --- --- TABLE dsprphotoq --- -CREATE TABLE IF NOT EXISTS `dsprphotoq` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, - `uid` int(11) NOT NULL DEFAULT 0, - `msg` mediumtext NOT NULL, - `attempt` tinyint(4) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `contact` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), + UNIQUE INDEX `cmd_item_contact` (`cmd`,`item`,`contact`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE event -- CREATE TABLE IF NOT EXISTS `event` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, + `guid` varchar(255) NOT NULL DEFAULT '', `uid` int(11) NOT NULL DEFAULT 0, `cid` int(11) NOT NULL DEFAULT 0, `uri` varchar(255) NOT NULL DEFAULT '', @@ -212,25 +229,27 @@ CREATE TABLE IF NOT EXISTS `event` ( `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `start` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `finish` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `summary` text NOT NULL, - `desc` text NOT NULL, - `location` text NOT NULL, + `summary` text, + `desc` text, + `location` text, `type` varchar(255) NOT NULL DEFAULT '', `nofinish` tinyint(1) NOT NULL DEFAULT 0, `adjust` tinyint(1) NOT NULL DEFAULT 1, `ignore` tinyint(1) unsigned NOT NULL DEFAULT 0, - `allow_cid` mediumtext NOT NULL, - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, - INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; + `allow_cid` mediumtext, + `allow_gid` mediumtext, + `deny_cid` mediumtext, + `deny_gid` mediumtext, + PRIMARY KEY(`id`), + INDEX `uid_start` (`uid`,`start`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE fcontact -- CREATE TABLE IF NOT EXISTS `fcontact` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, + `guid` varchar(255) NOT NULL DEFAULT '', `url` varchar(255) NOT NULL DEFAULT '', `name` varchar(255) NOT NULL DEFAULT '', `photo` varchar(255) NOT NULL DEFAULT '', @@ -244,63 +263,69 @@ CREATE TABLE IF NOT EXISTS `fcontact` ( `priority` tinyint(1) NOT NULL DEFAULT 0, `network` varchar(32) NOT NULL DEFAULT '', `alias` varchar(255) NOT NULL DEFAULT '', - `pubkey` text NOT NULL, + `pubkey` text, `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - INDEX `addr` (`addr`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `addr` (`addr`(32)), + INDEX `url` (`url`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE ffinder -- CREATE TABLE IF NOT EXISTS `ffinder` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `cid` int(10) unsigned NOT NULL DEFAULT 0, - `fid` int(10) unsigned NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `fid` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE fserver -- CREATE TABLE IF NOT EXISTS `fserver` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `server` varchar(255) NOT NULL DEFAULT '', `posturl` varchar(255) NOT NULL DEFAULT '', - `key` text NOT NULL, - INDEX `server` (`server`) -) DEFAULT CHARSET=utf8; + `key` text, + PRIMARY KEY(`id`), + INDEX `server` (`server`(32)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE fsuggest -- CREATE TABLE IF NOT EXISTS `fsuggest` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `cid` int(11) NOT NULL DEFAULT 0, `name` varchar(255) NOT NULL DEFAULT '', `url` varchar(255) NOT NULL DEFAULT '', `request` varchar(255) NOT NULL DEFAULT '', `photo` varchar(255) NOT NULL DEFAULT '', - `note` text NOT NULL, - `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' -) DEFAULT CHARSET=utf8; + `note` text, + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE gcign -- CREATE TABLE IF NOT EXISTS `gcign` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `gcid` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), INDEX `uid` (`uid`), INDEX `gcid` (`gcid`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE gcontact -- CREATE TABLE IF NOT EXISTS `gcontact` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `name` varchar(255) NOT NULL DEFAULT '', `nick` varchar(255) NOT NULL DEFAULT '', `url` varchar(255) NOT NULL DEFAULT '', @@ -312,66 +337,81 @@ CREATE TABLE IF NOT EXISTS `gcontact` ( `last_contact` datetime DEFAULT '0000-00-00 00:00:00', `last_failure` datetime DEFAULT '0000-00-00 00:00:00', `location` varchar(255) NOT NULL DEFAULT '', - `about` text NOT NULL, - `keywords` text NOT NULL, + `about` text, + `keywords` text, `gender` varchar(32) NOT NULL DEFAULT '', + `birthday` varchar(32) NOT NULL DEFAULT '0000-00-00', `community` tinyint(1) NOT NULL DEFAULT 0, + `contact-type` tinyint(1) NOT NULL DEFAULT -1, + `hide` tinyint(1) NOT NULL DEFAULT 0, + `nsfw` tinyint(1) NOT NULL DEFAULT 0, `network` varchar(255) NOT NULL DEFAULT '', `addr` varchar(255) NOT NULL DEFAULT '', + `notify` text, + `alias` varchar(255) NOT NULL DEFAULT '', `generation` tinyint(3) NOT NULL DEFAULT 0, `server_url` varchar(255) NOT NULL DEFAULT '', - INDEX `nurl` (`nurl`), + PRIMARY KEY(`id`), + INDEX `nurl` (`nurl`(64)), + INDEX `name` (`name`(64)), + INDEX `nick` (`nick`(32)), + INDEX `addr` (`addr`(64)), + INDEX `hide_network_updated` (`hide`,`network`,`updated`), INDEX `updated` (`updated`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE glink -- CREATE TABLE IF NOT EXISTS `glink` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `cid` int(11) NOT NULL DEFAULT 0, `uid` int(11) NOT NULL DEFAULT 0, `gcid` int(11) NOT NULL DEFAULT 0, `zcid` int(11) NOT NULL DEFAULT 0, `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - INDEX `cid_uid_gcid_zcid` (`cid`,`uid`,`gcid`,`zcid`), - INDEX `gcid` (`gcid`), - INDEX `zcid` (`zcid`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + UNIQUE INDEX `cid_uid_gcid_zcid` (`cid`,`uid`,`gcid`,`zcid`), + INDEX `gcid` (`gcid`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE group -- CREATE TABLE IF NOT EXISTS `group` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `visible` tinyint(1) NOT NULL DEFAULT 0, `deleted` tinyint(1) NOT NULL DEFAULT 0, `name` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY(`id`), INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE group_member -- CREATE TABLE IF NOT EXISTS `group_member` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `gid` int(10) unsigned NOT NULL DEFAULT 0, `contact-id` int(10) unsigned NOT NULL DEFAULT 0, - INDEX `uid_gid_contactid` (`uid`,`gid`,`contact-id`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `contactid` (`contact-id`), + INDEX `gid_contactid` (`gid`,`contact-id`), + UNIQUE INDEX `uid_gid_contactid` (`uid`,`gid`,`contact-id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE gserver -- CREATE TABLE IF NOT EXISTS `gserver` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `url` varchar(255) NOT NULL DEFAULT '', `nurl` varchar(255) NOT NULL DEFAULT '', `version` varchar(255) NOT NULL DEFAULT '', `site_name` varchar(255) NOT NULL DEFAULT '', - `info` text NOT NULL, + `info` text, `register_policy` tinyint(1) NOT NULL DEFAULT 0, `poco` varchar(255) NOT NULL DEFAULT '', `noscrape` varchar(255) NOT NULL DEFAULT '', @@ -381,61 +421,51 @@ CREATE TABLE IF NOT EXISTS `gserver` ( `last_poco_query` datetime DEFAULT '0000-00-00 00:00:00', `last_contact` datetime DEFAULT '0000-00-00 00:00:00', `last_failure` datetime DEFAULT '0000-00-00 00:00:00', - INDEX `nurl` (`nurl`) -) DEFAULT CHARSET=utf8; - --- --- TABLE guid --- -CREATE TABLE IF NOT EXISTS `guid` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, - `guid` varchar(255) NOT NULL DEFAULT '', - `plink` varchar(255) NOT NULL DEFAULT '', - `uri` varchar(255) NOT NULL DEFAULT '', - `network` varchar(32) NOT NULL DEFAULT '', - INDEX `guid` (`guid`), - INDEX `plink` (`plink`), - INDEX `uri` (`uri`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `nurl` (`nurl`(32)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE hook -- CREATE TABLE IF NOT EXISTS `hook` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `hook` varchar(255) NOT NULL DEFAULT '', `file` varchar(255) NOT NULL DEFAULT '', `function` varchar(255) NOT NULL DEFAULT '', `priority` int(11) unsigned NOT NULL DEFAULT 0, - INDEX `hook_file_function` (`hook`(30),`file`(60),`function`(30)) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + UNIQUE INDEX `hook_file_function` (`hook`(50),`file`(80),`function`(60)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE intro -- CREATE TABLE IF NOT EXISTS `intro` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `fid` int(11) NOT NULL DEFAULT 0, `contact-id` int(11) NOT NULL DEFAULT 0, `knowyou` tinyint(1) NOT NULL DEFAULT 0, `duplex` tinyint(1) NOT NULL DEFAULT 0, - `note` text NOT NULL, + `note` text, `hash` varchar(255) NOT NULL DEFAULT '', `datetime` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `blocked` tinyint(1) NOT NULL DEFAULT 1, - `ignore` tinyint(1) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `ignore` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE item -- CREATE TABLE IF NOT EXISTS `item` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `guid` varchar(255) NOT NULL DEFAULT '', `uri` varchar(255) NOT NULL DEFAULT '', `uid` int(10) unsigned NOT NULL DEFAULT 0, `contact-id` int(11) NOT NULL DEFAULT 0, + `gcontact-id` int(11) unsigned NOT NULL DEFAULT 0, `type` varchar(255) NOT NULL DEFAULT '', `wall` tinyint(1) NOT NULL DEFAULT 0, `gravity` tinyint(1) NOT NULL DEFAULT 0, @@ -448,34 +478,36 @@ CREATE TABLE IF NOT EXISTS `item` ( `commented` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `received` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `changed` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `owner-id` int(11) NOT NULL DEFAULT 0, `owner-name` varchar(255) NOT NULL DEFAULT '', `owner-link` varchar(255) NOT NULL DEFAULT '', `owner-avatar` varchar(255) NOT NULL DEFAULT '', + `author-id` int(11) NOT NULL DEFAULT 0, `author-name` varchar(255) NOT NULL DEFAULT '', `author-link` varchar(255) NOT NULL DEFAULT '', `author-avatar` varchar(255) NOT NULL DEFAULT '', `title` varchar(255) NOT NULL DEFAULT '', - `body` mediumtext NOT NULL, + `body` mediumtext, `app` varchar(255) NOT NULL DEFAULT '', `verb` varchar(255) NOT NULL DEFAULT '', `object-type` varchar(255) NOT NULL DEFAULT '', - `object` text NOT NULL, + `object` text, `target-type` varchar(255) NOT NULL DEFAULT '', - `target` text NOT NULL, - `postopts` text NOT NULL, + `target` text, + `postopts` text, `plink` varchar(255) NOT NULL DEFAULT '', `resource-id` varchar(255) NOT NULL DEFAULT '', `event-id` int(11) NOT NULL DEFAULT 0, - `tag` mediumtext NOT NULL, - `attach` mediumtext NOT NULL, - `inform` mediumtext NOT NULL, - `file` mediumtext NOT NULL, + `tag` mediumtext, + `attach` mediumtext, + `inform` mediumtext, + `file` mediumtext, `location` varchar(255) NOT NULL DEFAULT '', `coord` varchar(255) NOT NULL DEFAULT '', - `allow_cid` mediumtext NOT NULL, - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, + `allow_cid` mediumtext, + `allow_gid` mediumtext, + `deny_cid` mediumtext, + `deny_gid` mediumtext, `private` tinyint(1) NOT NULL DEFAULT 0, `pubmail` tinyint(1) NOT NULL DEFAULT 0, `moderated` tinyint(1) NOT NULL DEFAULT 0, @@ -491,33 +523,27 @@ CREATE TABLE IF NOT EXISTS `item` ( `mention` tinyint(1) NOT NULL DEFAULT 0, `network` varchar(32) NOT NULL DEFAULT '', `rendered-hash` varchar(32) NOT NULL DEFAULT '', - `rendered-html` mediumtext NOT NULL, + `rendered-html` mediumtext, `global` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), INDEX `guid` (`guid`), INDEX `uri` (`uri`), INDEX `parent` (`parent`), INDEX `parent-uri` (`parent-uri`), INDEX `extid` (`extid`), INDEX `uid_id` (`uid`,`id`), + INDEX `uid_contactid_id` (`uid`,`contact-id`,`id`), INDEX `uid_created` (`uid`,`created`), - INDEX `uid_unseen` (`uid`,`unseen`), + INDEX `uid_unseen_contactid` (`uid`,`unseen`,`contact-id`), INDEX `uid_network_received` (`uid`,`network`,`received`), - INDEX `uid_received` (`uid`,`received`), INDEX `uid_network_commented` (`uid`,`network`,`commented`), - INDEX `uid_commented` (`uid`,`commented`), - INDEX `uid_title` (`uid`,`title`), INDEX `uid_thrparent` (`uid`,`thr-parent`), INDEX `uid_parenturi` (`uid`,`parent-uri`), INDEX `uid_contactid_created` (`uid`,`contact-id`,`created`), - INDEX `wall_body` (`wall`,`body`(6)), - INDEX `uid_visible_moderated_created` (`uid`,`visible`,`moderated`,`created`), + INDEX `authorid_created` (`author-id`,`created`), INDEX `uid_uri` (`uid`,`uri`), - INDEX `uid_wall_created` (`uid`,`wall`,`created`), INDEX `resource-id` (`resource-id`), - INDEX `uid_type` (`uid`,`type`), - INDEX `uid_starred` (`uid`,`starred`), INDEX `contactid_allowcid_allowpid_denycid_denygid` (`contact-id`,`allow_cid`(10),`allow_gid`(10),`deny_cid`(10),`deny_gid`(10)), - INDEX `uid_wall_parent_created` (`uid`,`wall`,`parent`,`created`), INDEX `uid_type_changed` (`uid`,`type`,`changed`), INDEX `contactid_verb` (`contact-id`,`verb`), INDEX `deleted_changed` (`deleted`,`changed`), @@ -525,38 +551,40 @@ CREATE TABLE IF NOT EXISTS `item` ( INDEX `uid_eventid` (`uid`,`event-id`), INDEX `uid_authorlink` (`uid`,`author-link`), INDEX `uid_ownerlink` (`uid`,`owner-link`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE item_id -- CREATE TABLE IF NOT EXISTS `item_id` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `iid` int(11) NOT NULL DEFAULT 0, `uid` int(11) NOT NULL DEFAULT 0, `sid` varchar(255) NOT NULL DEFAULT '', `service` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY(`id`), INDEX `uid` (`uid`), INDEX `sid` (`sid`), - INDEX `service` (`service`), + INDEX `service` (`service`(32)), INDEX `iid` (`iid`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE locks -- CREATE TABLE IF NOT EXISTS `locks` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `name` varchar(128) NOT NULL DEFAULT '', `locked` tinyint(1) NOT NULL DEFAULT 0, - `created` datetime DEFAULT '0000-00-00 00:00:00' -) DEFAULT CHARSET=utf8; + `created` datetime DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE mail -- CREATE TABLE IF NOT EXISTS `mail` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `guid` varchar(64) NOT NULL DEFAULT '', `from-name` varchar(255) NOT NULL DEFAULT '', @@ -565,7 +593,7 @@ CREATE TABLE IF NOT EXISTS `mail` ( `contact-id` varchar(255) NOT NULL DEFAULT '', `convid` int(11) unsigned NOT NULL DEFAULT 0, `title` varchar(255) NOT NULL DEFAULT '', - `body` mediumtext NOT NULL, + `body` mediumtext, `seen` tinyint(1) NOT NULL DEFAULT 0, `reply` tinyint(1) NOT NULL DEFAULT 0, `replied` tinyint(1) NOT NULL DEFAULT 0, @@ -573,55 +601,56 @@ CREATE TABLE IF NOT EXISTS `mail` ( `uri` varchar(255) NOT NULL DEFAULT '', `parent-uri` varchar(255) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - INDEX `uid` (`uid`), - INDEX `guid` (`guid`), + PRIMARY KEY(`id`), + INDEX `uid_seen` (`uid`,`seen`), INDEX `convid` (`convid`), - INDEX `reply` (`reply`), - INDEX `uri` (`uri`), - INDEX `parent-uri` (`parent-uri`) -) DEFAULT CHARSET=utf8; + INDEX `uri` (`uri`(64)), + INDEX `parent-uri` (`parent-uri`(64)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE mailacct -- CREATE TABLE IF NOT EXISTS `mailacct` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `server` varchar(255) NOT NULL DEFAULT '', `port` int(11) NOT NULL DEFAULT 0, `ssltype` varchar(16) NOT NULL DEFAULT '', `mailbox` varchar(255) NOT NULL DEFAULT '', `user` varchar(255) NOT NULL DEFAULT '', - `pass` text NOT NULL, + `pass` text, `reply_to` varchar(255) NOT NULL DEFAULT '', `action` int(11) NOT NULL DEFAULT 0, `movetofolder` varchar(255) NOT NULL DEFAULT '', `pubmail` tinyint(1) NOT NULL DEFAULT 0, - `last_check` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' -) DEFAULT CHARSET=utf8; + `last_check` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE manage -- CREATE TABLE IF NOT EXISTS `manage` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `mid` int(11) NOT NULL DEFAULT 0, - INDEX `uid_mid` (`uid`,`mid`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + UNIQUE INDEX `uid_mid` (`uid`,`mid`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE notify -- CREATE TABLE IF NOT EXISTS `notify` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `hash` varchar(64) NOT NULL DEFAULT '', `type` int(11) NOT NULL DEFAULT 0, `name` varchar(255) NOT NULL DEFAULT '', `url` varchar(255) NOT NULL DEFAULT '', `photo` varchar(255) NOT NULL DEFAULT '', `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `msg` mediumtext NOT NULL, + `msg` mediumtext, `uid` int(11) NOT NULL DEFAULT 0, `link` varchar(255) NOT NULL DEFAULT '', `iid` int(11) NOT NULL DEFAULT 0, @@ -629,39 +658,69 @@ CREATE TABLE IF NOT EXISTS `notify` ( `seen` tinyint(1) NOT NULL DEFAULT 0, `verb` varchar(255) NOT NULL DEFAULT '', `otype` varchar(16) NOT NULL DEFAULT '', - INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; + `name_cache` tinytext, + `msg_cache` mediumtext, + PRIMARY KEY(`id`), + INDEX `hash_uid` (`hash`,`uid`), + INDEX `seen_uid_date` (`seen`,`uid`,`date`), + INDEX `uid_date` (`uid`,`date`), + INDEX `uid_type_link` (`uid`,`type`,`link`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE notify-threads -- CREATE TABLE IF NOT EXISTS `notify-threads` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `notify-id` int(11) NOT NULL DEFAULT 0, `master-parent-item` int(10) unsigned NOT NULL DEFAULT 0, `parent-item` int(10) unsigned NOT NULL DEFAULT 0, `receiver-uid` int(11) NOT NULL DEFAULT 0, - INDEX `master-parent-item` (`master-parent-item`), - INDEX `receiver-uid` (`receiver-uid`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; + +-- +-- TABLE oembed +-- +CREATE TABLE IF NOT EXISTS `oembed` ( + `url` varbinary(255) NOT NULL, + `content` mediumtext, + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`url`), + INDEX `created` (`created`) +) DEFAULT CHARSET=utf8mb4; + +-- +-- TABLE parsed_url +-- +CREATE TABLE IF NOT EXISTS `parsed_url` ( + `url` varbinary(255) NOT NULL, + `guessing` tinyint(1) NOT NULL DEFAULT 0, + `oembed` tinyint(1) NOT NULL DEFAULT 0, + `content` mediumtext, + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`url`,`guessing`,`oembed`), + INDEX `created` (`created`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE pconfig -- CREATE TABLE IF NOT EXISTS `pconfig` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, - `cat` varchar(255) NOT NULL DEFAULT '', - `k` varchar(255) NOT NULL DEFAULT '', - `v` mediumtext NOT NULL, - INDEX `uid_cat_k` (`uid`,`cat`(30),`k`(30)) -) DEFAULT CHARSET=utf8; + `cat` varbinary(255) NOT NULL DEFAULT '', + `k` varbinary(255) NOT NULL DEFAULT '', + `v` mediumtext, + PRIMARY KEY(`id`), + UNIQUE INDEX `uid_cat_k` (`uid`,`cat`,`k`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE photo -- CREATE TABLE IF NOT EXISTS `photo` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `contact-id` int(10) unsigned NOT NULL DEFAULT 0, `guid` varchar(64) NOT NULL DEFAULT '', @@ -669,7 +728,7 @@ CREATE TABLE IF NOT EXISTS `photo` ( `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `title` varchar(255) NOT NULL DEFAULT '', - `desc` text NOT NULL, + `desc` text, `album` varchar(255) NOT NULL DEFAULT '', `filename` varchar(255) NOT NULL DEFAULT '', `type` varchar(128) NOT NULL DEFAULT 'image/jpeg', @@ -679,50 +738,66 @@ CREATE TABLE IF NOT EXISTS `photo` ( `data` mediumblob NOT NULL, `scale` tinyint(3) NOT NULL DEFAULT 0, `profile` tinyint(1) NOT NULL DEFAULT 0, - `allow_cid` mediumtext NOT NULL, - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, - INDEX `uid` (`uid`), - INDEX `resource-id` (`resource-id`), - INDEX `guid` (`guid`) -) DEFAULT CHARSET=utf8; + `allow_cid` mediumtext, + `allow_gid` mediumtext, + `deny_cid` mediumtext, + `deny_gid` mediumtext, + PRIMARY KEY(`id`), + INDEX `uid_contactid` (`uid`,`contact-id`), + INDEX `uid_profile` (`uid`,`profile`), + INDEX `uid_album_scale_created` (`uid`,`album`(32),`scale`,`created`), + INDEX `uid_album_resource-id_created` (`uid`,`album`(32),`resource-id`(64),`created`), + INDEX `resource-id` (`resource-id`(64)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE poll -- CREATE TABLE IF NOT EXISTS `poll` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, - `q0` mediumtext NOT NULL, - `q1` mediumtext NOT NULL, - `q2` mediumtext NOT NULL, - `q3` mediumtext NOT NULL, - `q4` mediumtext NOT NULL, - `q5` mediumtext NOT NULL, - `q6` mediumtext NOT NULL, - `q7` mediumtext NOT NULL, - `q8` mediumtext NOT NULL, - `q9` mediumtext NOT NULL, + `q0` text, + `q1` text, + `q2` text, + `q3` text, + `q4` text, + `q5` text, + `q6` text, + `q7` text, + `q8` text, + `q9` text, + PRIMARY KEY(`id`), INDEX `uid` (`uid`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE poll_result -- CREATE TABLE IF NOT EXISTS `poll_result` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `poll_id` int(11) NOT NULL DEFAULT 0, `choice` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), INDEX `poll_id` (`poll_id`), INDEX `choice` (`choice`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; + +-- +-- TABLE process +-- +CREATE TABLE IF NOT EXISTS `process` ( + `pid` int(10) unsigned NOT NULL, + `command` varbinary(32) NOT NULL DEFAULT '', + `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`pid`), + INDEX `command` (`command`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE profile -- CREATE TABLE IF NOT EXISTS `profile` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `profile-name` varchar(255) NOT NULL DEFAULT '', `is-default` tinyint(1) NOT NULL DEFAULT 0, @@ -738,148 +813,156 @@ CREATE TABLE IF NOT EXISTS `profile` ( `hometown` varchar(255) NOT NULL DEFAULT '', `gender` varchar(32) NOT NULL DEFAULT '', `marital` varchar(255) NOT NULL DEFAULT '', - `with` text NOT NULL, + `with` text, `howlong` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `sexual` varchar(255) NOT NULL DEFAULT '', `politic` varchar(255) NOT NULL DEFAULT '', `religion` varchar(255) NOT NULL DEFAULT '', - `pub_keywords` text NOT NULL, - `prv_keywords` text NOT NULL, - `likes` text NOT NULL, - `dislikes` text NOT NULL, - `about` text NOT NULL, + `pub_keywords` text, + `prv_keywords` text, + `likes` text, + `dislikes` text, + `about` text, `summary` varchar(255) NOT NULL DEFAULT '', - `music` text NOT NULL, - `book` text NOT NULL, - `tv` text NOT NULL, - `film` text NOT NULL, - `interest` text NOT NULL, - `romance` text NOT NULL, - `work` text NOT NULL, - `education` text NOT NULL, - `contact` text NOT NULL, + `music` text, + `book` text, + `tv` text, + `film` text, + `interest` text, + `romance` text, + `work` text, + `education` text, + `contact` text, `homepage` varchar(255) NOT NULL DEFAULT '', + `xmpp` varchar(255) NOT NULL DEFAULT '', `photo` varchar(255) NOT NULL DEFAULT '', `thumb` varchar(255) NOT NULL DEFAULT '', `publish` tinyint(1) NOT NULL DEFAULT 0, `net-publish` tinyint(1) NOT NULL DEFAULT 0, - INDEX `hometown` (`hometown`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `uid_is-default` (`uid`,`is-default`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE profile_check -- CREATE TABLE IF NOT EXISTS `profile_check` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `uid` int(10) unsigned NOT NULL DEFAULT 0, `cid` int(10) unsigned NOT NULL DEFAULT 0, `dfrn_id` varchar(255) NOT NULL DEFAULT '', `sec` varchar(255) NOT NULL DEFAULT '', - `expire` int(11) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; + `expire` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE push_subscriber -- CREATE TABLE IF NOT EXISTS `push_subscriber` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `callback_url` varchar(255) NOT NULL DEFAULT '', `topic` varchar(255) NOT NULL DEFAULT '', `nickname` varchar(255) NOT NULL DEFAULT '', `push` int(11) NOT NULL DEFAULT 0, `last_update` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `secret` varchar(255) NOT NULL DEFAULT '' -) DEFAULT CHARSET=utf8; + `secret` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE queue -- CREATE TABLE IF NOT EXISTS `queue` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `cid` int(11) NOT NULL DEFAULT 0, `network` varchar(32) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `last` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `content` mediumtext NOT NULL, + `content` mediumtext, `batch` tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`), INDEX `cid` (`cid`), INDEX `created` (`created`), INDEX `last` (`last`), INDEX `network` (`network`), INDEX `batch` (`batch`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE register -- CREATE TABLE IF NOT EXISTS `register` ( - `id` int(11) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(11) unsigned NOT NULL auto_increment, `hash` varchar(255) NOT NULL DEFAULT '', `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `uid` int(11) unsigned NOT NULL DEFAULT 0, `password` varchar(255) NOT NULL DEFAULT '', - `language` varchar(16) NOT NULL DEFAULT '' -) DEFAULT CHARSET=utf8; + `language` varchar(16) NOT NULL DEFAULT '', + `note` text, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE search -- CREATE TABLE IF NOT EXISTS `search` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `term` varchar(255) NOT NULL DEFAULT '', - INDEX `uid` (`uid`), - INDEX `term` (`term`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `uid` (`uid`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE session -- CREATE TABLE IF NOT EXISTS `session` ( - `id` bigint(20) unsigned NOT NULL auto_increment PRIMARY KEY, - `sid` varchar(255) NOT NULL DEFAULT '', - `data` text NOT NULL, + `id` bigint(20) unsigned NOT NULL auto_increment, + `sid` varbinary(255) NOT NULL DEFAULT '', + `data` text, `expire` int(10) unsigned NOT NULL DEFAULT 0, - INDEX `sid` (`sid`), + PRIMARY KEY(`id`), + INDEX `sid` (`sid`(64)), INDEX `expire` (`expire`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE sign -- CREATE TABLE IF NOT EXISTS `sign` ( - `id` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `id` int(10) unsigned NOT NULL auto_increment, `iid` int(10) unsigned NOT NULL DEFAULT 0, - `retract_iid` int(10) unsigned NOT NULL DEFAULT 0, - `signed_text` mediumtext NOT NULL, - `signature` text NOT NULL, + `signed_text` mediumtext, + `signature` text, `signer` varchar(255) NOT NULL DEFAULT '', - INDEX `iid` (`iid`), - INDEX `retract_iid` (`retract_iid`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `iid` (`iid`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE spam -- CREATE TABLE IF NOT EXISTS `spam` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `uid` int(11) NOT NULL DEFAULT 0, `spam` int(11) NOT NULL DEFAULT 0, `ham` int(11) NOT NULL DEFAULT 0, `term` varchar(255) NOT NULL DEFAULT '', `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + PRIMARY KEY(`id`), INDEX `uid` (`uid`), INDEX `spam` (`spam`), INDEX `ham` (`ham`), INDEX `term` (`term`) -) DEFAULT CHARSET=utf8; +) DEFAULT CHARSET=utf8mb4; -- -- TABLE term -- CREATE TABLE IF NOT EXISTS `term` ( - `tid` int(10) unsigned NOT NULL auto_increment PRIMARY KEY, + `tid` int(10) unsigned NOT NULL auto_increment, `oid` int(10) unsigned NOT NULL DEFAULT 0, `otype` tinyint(3) unsigned NOT NULL DEFAULT 0, `type` tinyint(3) unsigned NOT NULL DEFAULT 0, @@ -891,21 +974,23 @@ CREATE TABLE IF NOT EXISTS `term` ( `global` tinyint(1) NOT NULL DEFAULT 0, `aid` int(10) unsigned NOT NULL DEFAULT 0, `uid` int(10) unsigned NOT NULL DEFAULT 0, + PRIMARY KEY(`tid`), INDEX `oid_otype_type_term` (`oid`,`otype`,`type`,`term`), - INDEX `uid_term_tid` (`uid`,`term`,`tid`), - INDEX `type_term` (`type`,`term`), - INDEX `uid_otype_type_term_global_created` (`uid`,`otype`,`type`,`term`,`global`,`created`), - INDEX `otype_type_term_tid` (`otype`,`type`,`term`,`tid`), - INDEX `guid` (`guid`) -) DEFAULT CHARSET=utf8; + INDEX `uid_otype_type_term_global_created` (`uid`,`otype`,`type`,`term`(32),`global`,`created`), + INDEX `uid_otype_type_url` (`uid`,`otype`,`type`,`url`(64)), + INDEX `guid` (`guid`(64)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE thread -- CREATE TABLE IF NOT EXISTS `thread` ( - `iid` int(10) unsigned NOT NULL DEFAULT 0 PRIMARY KEY, + `iid` int(10) unsigned NOT NULL DEFAULT 0, `uid` int(10) unsigned NOT NULL DEFAULT 0, `contact-id` int(11) unsigned NOT NULL DEFAULT 0, + `gcontact-id` int(11) unsigned NOT NULL DEFAULT 0, + `owner-id` int(11) unsigned NOT NULL DEFAULT 0, + `author-id` int(11) unsigned NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `commented` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', @@ -926,48 +1011,34 @@ CREATE TABLE IF NOT EXISTS `thread` ( `forum_mode` tinyint(1) NOT NULL DEFAULT 0, `mention` tinyint(1) NOT NULL DEFAULT 0, `network` varchar(32) NOT NULL DEFAULT '', - INDEX `created` (`created`), - INDEX `commented` (`commented`), + PRIMARY KEY(`iid`), INDEX `uid_network_commented` (`uid`,`network`,`commented`), INDEX `uid_network_created` (`uid`,`network`,`created`), INDEX `uid_contactid_commented` (`uid`,`contact-id`,`commented`), INDEX `uid_contactid_created` (`uid`,`contact-id`,`created`), - INDEX `wall_private_received` (`wall`,`private`,`received`), INDEX `uid_created` (`uid`,`created`), - INDEX `uid_commented` (`uid`,`commented`) -) DEFAULT CHARSET=utf8; + INDEX `uid_commented` (`uid`,`commented`), + INDEX `uid_wall_created` (`uid`,`wall`,`created`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE tokens -- CREATE TABLE IF NOT EXISTS `tokens` ( - `id` varchar(40) NOT NULL PRIMARY KEY, - `secret` text NOT NULL, + `id` varchar(40) NOT NULL, + `secret` text, `client_id` varchar(20) NOT NULL DEFAULT '', `expires` int(11) NOT NULL DEFAULT 0, `scope` varchar(200) NOT NULL DEFAULT '', - `uid` int(11) NOT NULL DEFAULT 0 -) DEFAULT CHARSET=utf8; - --- --- TABLE unique_contacts --- -CREATE TABLE IF NOT EXISTS `unique_contacts` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, - `url` varchar(255) NOT NULL DEFAULT '', - `nick` varchar(255) NOT NULL DEFAULT '', - `name` varchar(255) NOT NULL DEFAULT '', - `avatar` varchar(255) NOT NULL DEFAULT '', - `location` varchar(255) NOT NULL DEFAULT '', - `about` text NOT NULL, - INDEX `url` (`url`) -) DEFAULT CHARSET=utf8; + `uid` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE user -- CREATE TABLE IF NOT EXISTS `user` ( - `uid` int(11) NOT NULL auto_increment PRIMARY KEY, + `uid` int(11) NOT NULL auto_increment, `guid` varchar(64) NOT NULL DEFAULT '', `username` varchar(255) NOT NULL DEFAULT '', `password` varchar(255) NOT NULL DEFAULT '', @@ -981,10 +1052,10 @@ CREATE TABLE IF NOT EXISTS `user` ( `default-location` varchar(255) NOT NULL DEFAULT '', `allow_location` tinyint(1) NOT NULL DEFAULT 0, `theme` varchar(255) NOT NULL DEFAULT '', - `pubkey` text NOT NULL, - `prvkey` text NOT NULL, - `spubkey` text NOT NULL, - `sprvkey` text NOT NULL, + `pubkey` text, + `prvkey` text, + `spubkey` text, + `sprvkey` text, `verified` tinyint(1) unsigned NOT NULL DEFAULT 0, `blocked` tinyint(1) unsigned NOT NULL DEFAULT 0, `blockwall` tinyint(1) unsigned NOT NULL DEFAULT 0, @@ -994,6 +1065,7 @@ CREATE TABLE IF NOT EXISTS `user` ( `cntunkmail` int(11) NOT NULL DEFAULT 10, `notify-flags` int(11) unsigned NOT NULL DEFAULT 65535, `page-flags` int(11) unsigned NOT NULL DEFAULT 0, + `account-type` int(11) unsigned NOT NULL DEFAULT 0, `prvnets` tinyint(1) NOT NULL DEFAULT 0, `pwdreset` varchar(255) NOT NULL DEFAULT '', `maxreq` int(11) NOT NULL DEFAULT 10, @@ -1004,33 +1076,35 @@ CREATE TABLE IF NOT EXISTS `user` ( `expire_notification_sent` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `service_class` varchar(32) NOT NULL DEFAULT '', `def_gid` int(11) NOT NULL DEFAULT 0, - `allow_cid` mediumtext NOT NULL, - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, - `openidserver` text NOT NULL, - INDEX `nickname` (`nickname`) -) DEFAULT CHARSET=utf8; + `allow_cid` mediumtext, + `allow_gid` mediumtext, + `deny_cid` mediumtext, + `deny_gid` mediumtext, + `openidserver` text, + PRIMARY KEY(`uid`), + INDEX `nickname` (`nickname`(32)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE userd -- CREATE TABLE IF NOT EXISTS `userd` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, + `id` int(11) NOT NULL auto_increment, `username` varchar(255) NOT NULL, - INDEX `username` (`username`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`), + INDEX `username` (`username`(32)) +) DEFAULT CHARSET=utf8mb4; -- -- TABLE workerqueue -- CREATE TABLE IF NOT EXISTS `workerqueue` ( - `id` int(11) NOT NULL auto_increment PRIMARY KEY, - `parameter` text NOT NULL, + `id` int(11) NOT NULL auto_increment, + `parameter` text, `priority` tinyint(3) unsigned NOT NULL DEFAULT 0, `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `pid` int(11) NOT NULL DEFAULT 0, `executed` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - INDEX `created` (`created`) -) DEFAULT CHARSET=utf8; + PRIMARY KEY(`id`) +) DEFAULT CHARSET=utf8mb4; diff --git a/doc/Accesskeys.md b/doc/Accesskeys.md index c49e79c0ab..24b4dd4e7d 100644 --- a/doc/Accesskeys.md +++ b/doc/Accesskeys.md @@ -1,6 +1,8 @@ Accesskeys in Friendica ======================= +* [Home](help) + General ------- * p: profile @@ -37,10 +39,7 @@ General * o: Profile * t: Contacts * d: Common friends -* b: Toggle Blocked status -* i: Toggle Ignored status -* v: Toggle Archive status -* r: Repair +* r: Advanced /message -------- diff --git a/doc/BBCode.md b/doc/BBCode.md index fe7c1481f6..50fb406b05 100644 --- a/doc/BBCode.md +++ b/doc/BBCode.md @@ -1,154 +1,616 @@ Friendica BBCode tags reference ======================== -* [Home](help) +* [Creating posts](help/Text_editor) -Inline ------ +## Inline + -
[u]underlined[/u]
: underlined + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[b]bold[/b]bold
[i]italic[/i]italic
[u]underlined[/u]underlined
[s]strike[/s]strike
[o]overline[/o]overline
[color=red]red[/color]red
[url=http://www.friendica.com]Friendica[/url]Friendica
[img]http://friendica.com/sites/default/files/friendika-32.png[/img]Immagine/foto
[img=64x32]http://friendica.com/sites/default/files/friendika-32.png[/img]
+
Note: provided height is simply discarded.
[size=xx-small]small text[/size]small text
[size=xx-large]big text[/size]big text
[size=20]exact size[/size] (size can be any number, in pixel)exact size
[font=serif]Serif font[/font]Serif font
-
[s]strike[/s]
: strike +### Links -
[color=red]red[/color]
: red + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[url]http://friendica.com[/url]http://friendica.com
[url=http://friendica.com]Friendica[/url]Friendica
[bookmark]http://friendica.com[/bookmark]

+#^[url]http://friendica.com[/url]

Friendica: http://friendica.com

[bookmark=http://friendica.com]Bookmark[/bookmark]

+#^[url=http://friendica.com]Bookmark[/url]

+#[url=http://friendica.com]^[/url][url=http://friendica.com]Bookmark[/url]

Friendica: Bookmark

[url=/posts/f16d77b0630f0134740c0cc47a0ea02a]Diaspora post with GUID[/url]Diaspora post with GUID
#Friendica#Friendica
@Mention@Mention
acct:account@friendica.host.com (WebFinger)acct:account@friendica.host.com
[mail]user@mail.example.com[/mail]user@mail.example.com
[mail=user@mail.example.com]Send an email to User[/mail]Send an email to User
-
[url=http://www.friendica.com]Friendica[/url]
: Friendica +## Blocks -
[img]http://friendica.com/sites/default/files/friendika-32.png[/img]
: Immagine/foto + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[p]A paragraph of text[/p]

A paragraph of text

Inline [code]code[/code] in a paragraphInline code in a paragraph
[code]Multi
line
code[/code]
Multi +line +code
[code=php]function text_highlight($s,$lang)[/code]
  1.  function text_highlight($s,$lang)
[quote]quote[/quote]
quote
[quote=Author]Author? Me? No, no, no...[/quote]Author wrote:
Author? Me? No, no, no...
[center]Centered text[/center]
Centered text
You should not read any further if you want to be surprised.[spoiler]There is a happy end.[/spoiler] +
+ You should not read any further if you want to be surprised.
+ Click to open/close + +
+
+
[spoiler=Author]Spoiler quote[/spoiler] +
+ Author wrote:
+ Click to open/close + +
+
+
[hr] (horizontal line)
-
[size=xx-small]small text[/size]
: small text +### Titles -
[size=xx-large]big text[/size]
: big text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[h1]Title 1[/h1]

Title 1

[h2]Title 2[/h2]

Title 2

[h3]Title 3[/h3]

Title 3

[h4]Title 4[/h4]

Title 4

[h5]Title 5[/h5]
Title 5
[h6]Title 6[/h6]
Title 6
-
[size=20]exact size[/size] (size can be any number, in pixel)
: exact size +### Tables + + + + + + + + + + + + + + + + + +
BBCodeResult
[table]
+  [tr]
+    [th]Header 1[/th]
+    [th]Header 2[/th]
+    [th]Header 2[/th]
+  [/tr]
+  [tr]
+    [td]Cell 1[/td]
+    [td]Cell 2[/td]
+    [td]Cell 3[/td]
+  [/tr]
+  [tr]
+    [td]Cell 4[/td]
+    [td]Cell 5[/td]
+    [td]Cell 6[/td]
+  [/tr]
+[/table]
+ + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3
Cell 1Cell 2Cell 3
Cell 4Cell 5Cell 6
+
[table border=0] + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3
Cell 1Cell 2Cell 3
Cell 4Cell 5Cell 6
+
[table border=1] + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3
Cell 1Cell 2Cell 3
Cell 4Cell 5Cell 6
+
+### Lists + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[ul]
+  [li] First list element
+  [li] Second list element
+[/ul]
+[list]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[ol]
+  [*] First list element
+  [*] Second list element
+[/ol]
+[list=1]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[list=]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[list=i]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[list=I]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[list=a]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
[list=A]
+  [*] First list element
+  [*] Second list element
+[/list]
+
    +
  • First list element
  • +
  • Second list element
  • +
+
- - - -Block ------ - -
[code]code[/code]
- -code - -

 

- -
[quote]quote[/quote]
- -
quote
- -

 

- -
[quote=Author]Author? Me? No, no, no...[/quote]
- -Author wrote:
Author? Me? No, no, no...
- -

 

- -
[center]centered text[/center]
- -
centered text
- -

 

- -
You should not read any further if you want to be surprised.[spoiler]There is a happy end.[/spoiler]
- -You should not read any further if you want to be surprised.
*click to open/close* - -(The text between thhe opening and the closing of the spoiler tag will be visible once the link is clicked. So *"There is a happy end."* wont be visible until the spoiler is uncovered.) - -

 

- -**Table** -
[table border=1]
- [tr] 
-   [th]Tables now[/th]
- [/tr]
- [tr]
-   [td]Have headers[/td]
- [/tr]
-[/table]
- -
Tables now
Have headers
- -

 

- -**List** - -
[list]
- [*] First list element
- [*] Second list element
-[/list]
- - -[list] is equivalent to [ul] (unordered list). - -[ol] can be used instead of [list] to show an ordered list: - -
[ol]
- [*] First list element
- [*] Second list element
-[/ol]
- - -For more options on ordered lists, you can define the style of numeration on [list] argument: -
[list=1]
: decimal - -
[list=i]
: lover case roman - -
[list=I]
: upper case roman - -
[list=a]
: lover case alphabetic - -
[list=A] 
: upper case alphabetic - - - - -Embed ------- +## Embed You can embed video, audio and more in a message. -
[video]url[/video]
-
[audio]url[/audio]
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
[video]url[/video]Where *url* can be an url to youtube, vimeo, soundcloud, or other sites wich supports oembed or opengraph specifications.
[video]Video file url[/video] +[audio]Audio file url[/audio]Full URL to an ogg/ogv/oga/ogm/webm/mp4/mp3 file. An HTML5 player will be used to show it.
[youtube]Youtube URL[/youtube]Youtube video OEmbed display. May not embed an actual player.
[youtube]Youtube video ID[/youtube]Youtube player iframe embed.
[vimeo]Vimeo URL[/vimeo]Vimeo video OEmbed display. May not embed an actual player.
[vimeo]Vimeo video ID[/vimeo]Vimeo player iframe embed.
[embed]URL[/embed]Embed OEmbed rich content.
[iframe]URL[/iframe]General embed, iframe size is limited by the theme size for video players.
[url]*url*[/url]If *url* supports oembed or opengraph specifications the embedded object will be shown (eg, documents from scribd). +Page title with a link to *url* will be shown.
-Where *url* can be an url to youtube, vimeo, soundcloud, or other sites wich supports oembed or opengraph specifications. -*url* can be also full url to an ogg file. HTML5 tag will be used to show it. +## Map -
[url]*url*[/url]
+This require "openstreetmap" or "Google Maps" addon version 1.3 or newer. +If the addon isn't activated, the raw coordinates are shown instead. -If *url* supports oembed or opengraph specifications the embedded object will be shown (eg, documents from scribd). -Page title with a link to *url* will be shown. + + + + + + + + + + + + + + + + + +
BBCodeResult
[map]address[/map]Embeds a map centered on this address.
[map=lat,long]Embeds a map centered on those coordinates.
[map]Embeds a map centered on the post's location.
-Map ---- +## Abstract for longer posts -
[map]address[/map]
-
[map=lat,long]
+If you want to spread your post to several third party networks you can have the problem that these networks have a length limitation like on Twitter. -You can embed maps from coordinates or addresses. -This require "openstreetmap" addon version 1.3 or newer. +Friendica is using a semi intelligent mechanism to generate a fitting abstract. +But it can be interesting to define a custom abstract that will only be displayed on the external network. +This is done with the [abstract]-element. + + + + + + + + + +
BBCodeResult
[abstract]Totally interesting! A must-see! Please click the link![/abstract]
+I want to tell you a really boring story that you really never wanted to hear.
Twitter would display the text
Totally interesting! A must-see! Please click the link!
+On Friendica you would only see the text after
I want to tell you a really ...
+It is even possible to define abstracts for separate networks: -Special -------- + + + + + + + + + +
BBCodeResult
+[abstract]Hi friends Here are my newest pictures![/abstract]
+[abstract=twit]Hi my dear Twitter followers. Do you want to see my new +pictures?[/abstract]
+[abstract=apdn]Helly my dear followers on ADN. I made sone new pictures +that I wanted to share with you.[/abstract]
+Today I was in the woods and took some real cool pictures ...
For Twitter and App.net the system will use the defined abstracts.
+For other networks (e.g. when you are using the "statusnet" connector that is used to post to your GNU Social account) the general abstract element will be used.
-If you need to put literal bbcode in a message, [noparse], [nobb] or [pre] are used to escape bbcode: +If you use (for example) the "buffer" connector to post to Facebook or Google+ you can use this element to define an abstract for a longer blogpost that you don't want to post completely to these networks. -
[noparse][b]bold[/b][/noparse]
: [b]bold[/b] +Networks like Facebook or Google+ aren't length limited. +For this reason the [abstract] element isn't used. +Instead you have to name the explicit network: + + + + + + + + + +
BBCodeResult
+[abstract]These days I had a strange encounter...[/abstract]
+[abstract=goog]Helly my dear Google+ followers. You have to read my newest blog post![/abstract]
+[abstract=face]Hello my Facebook friends. These days happened something really cool.[/abstract]
+While taking pictures in the woods I had a really strange encounter...
Google and Facebook will show the respective abstracts while the other networks will show the default one.
+
Meanwhile, Friendica won't show any of the abstracts.
+The [abstract] element isn't working with connectors where we post the HTML like Tumblr, Wordpress or Pump.io. +For the native connections--that is to e.g. Friendica, Hubzilla, Diaspora or GNU Social--the full posting is used and the contacts instance will display the posting as desired. + +## Special + + + + + + + + + + + + + + + + + + + + + + +
BBCodeResult
If you need to put literal bbcode in a message, [noparse], [nobb] or [pre] are used to escape bbcode: +
    +
  • [noparse][b]bold[/b][/noparse]
  • +
  • [nobb][b]bold[/b][/nobb]
  • +
  • [pre][b]bold[/b][/pre]
  • +
+
[b]bold[/b]
[nosmile] is used to disable smilies on a post by post basis
+
+ [nosmile] ;-) :-O +
;-) :-O
Custom inline styles
+
+[style=text-shadow: 0 0 4px #CC0000;]You can change all the CSS properties of this block.[/style]
You can change all the CSS properties of this block.
Custom class block
+
+[class=custom]If the class exists, this block will have the custom class style applied.[/class]
<span class="custom">If the class exists,
this block will have the custom class
style applied.</span>
diff --git a/doc/Bugs-and-Issues.md b/doc/Bugs-and-Issues.md index 366b2ed662..d316971d2a 100644 --- a/doc/Bugs-and-Issues.md +++ b/doc/Bugs-and-Issues.md @@ -6,8 +6,10 @@ Bugs and Issues If your server has a support page, you should report any bugs/issues you encounter there first. Reporting to your support page before reporting to the developers makes their job easier, as they don't have to deal with bug reports that might not have anything to do with them. This helps us get new features faster. +You can also contact the [friendica support forum](https://helpers.pyxis.uberspace.de/profile/helpers) and report your problem there. +Maybe someone from another node encountered the problem as well and can help you. -If you're a technical user, or your site doesn't have a support page, you'll need to use the [Bug Tracker](http://bugs.friendica.com/). +If you're a technical user, or your site doesn't have a support page, you'll need to use the [Bug Tracker](https://github.com/friendica/friendica/issues). Please perform a search to see if there's already an open bug that matches yours before submitting anything. Try to provide as much information as you can about the bug, including the **full** text of any error messages or notices, and any steps required to replicate the problem in as much detail as possible. diff --git a/doc/Connectors.md b/doc/Connectors.md index cd4b643f14..148352c552 100644 --- a/doc/Connectors.md +++ b/doc/Connectors.md @@ -57,13 +57,15 @@ All that the pages need to have is a discoverable feed using either the RSS or A Twitter --- -To follow a Twitter member, put the URL of the Twitter member's main page into the Connect box on your [Contacts](contacts) page. +To follow a Twitter member, the Twitter-Connector (Addon) needs to be configured on your node. +If this is the case put the URL of the Twitter member's main page into the Connect box on your [Contacts](contacts) page. To reply, you must have the Twitter connector installed, and reply using your own status editor. Begin the message with @twitterperson replacing with the Twitter username. Email --- +If the php module for IMAP support is available on your server, Friendica can connect to email contacts as well. Configure the email connector from your [Settings](settings) page. Once this has been done, you may enter an email address to connect with using the Connect box on your [Contacts](contacts) page. They must be the sender of a message which is currently in your INBOX for the connection to succeed. diff --git a/doc/Developers-Intro.md b/doc/Developers-Intro.md index 7e5caae2b3..61b300b0cc 100644 --- a/doc/Developers-Intro.md +++ b/doc/Developers-Intro.md @@ -16,7 +16,7 @@ Contact us The discussion of Friendica development takes place in the following Friendica forums: -* The main [forum for Friendica development](https://friendika.openmindspace.org/profile/friendicadevelopers) +* The main [forum for Friendica development](https://helpers.pyxis.uberspace.de/profile/developers) * The [forum for Friendica theme development](https://friendica.eu/profile/ftdevs) Help other users @@ -29,7 +29,7 @@ Welcome them, answer their questions, point them to documentation or ping other Translation --- -The documentation contains help on how to translate Friendica in the [at Transifex](/help/translations) where the UI is translated. +The documentation contains help on how to translate Friendica [at Transifex](/help/translations) where the UI is translated. If you don't want to translate the UI, or it is already done to your satisfaction, you might want to work on the translation of the /help files? Design @@ -42,16 +42,66 @@ If you have seen Friendica you probably have ideas to improve it, haven't you? * Make plans for a better Friendica interface design and share them with us. * Tell us if you are able to realize your ideas or what kind of help you need. We can't promise we have the right skills in the group but we'll try. -* Choose a thing to start with, e.g. work on the icon set of your favourite theme +* Choose a thing to start with, e.g. work on the icon set of your favorite theme Programming --- +###Coding standards + +For the sake of consistency between contribution and general code readability, Friendica follows the widespread [PSR-2 coding standards](http://www.php-fig.org/psr/psr-2/) to the exception of a few rules. +Here's a few primers if you are new to Friendica or to the PSR-2 coding standards: + * Indentation is tabs, period (not PSR-2). + * By default, strings are enclosed in single quotes, but feel free to use double quotes if it makes more sense (SQL queries, adding tabs and line feeds). + * Operators are wrapped by spaces, e.g. `$var === true`, `$var = 1 + 2` and `'string' . $concat . 'enation'` + * Braces are mandatory in conditions + * No closing PHP tag + * No trailing spaces + +Don't worry, you don't have to know by heart the PSR-2 coding standards to start contributing to Friendica. +There are a few tools you can use to check or fix your files before you commit. + +For documentation we use the standard of *one sentence per line* for the `md` files in the `/doc` and `/doc/$lng` subdirectories. + +####Check with [PHP Code Sniffer](https://github.com/squizlabs/PHP_CodeSniffer) + +This tool checks your files against a variety of coding standards, including PSR-2, and ouputs a report of all the standard violations. +You can simply install it through PEAR: `pear install PHP_CodeSniffer` +Once it is installed and available in your PATH, here's the command to run before committing your work: + + $> phpcs --standard=PSR2 + +The output is a list of all the coding standards violations that you should fix before committing your work. +Additionally, `phpcs` integrates with a few IDEs (Eclipse, Netbeans, PHPStorm...) so that you don't have to fiddle with the command line. + +####Fix with PHP Code Beautifier and Fixer (phpbcf) included in PHP Code Sniffer + +If you're getting a massive list of standards violations when running `phpcs`, it can be annoying to fix all the violations by hand. +Thankfully, PHP Code Sniffer is shipped with an automatic code fixer that can take care of the tedious task for you. +Here's the command to automatically fix the files you created/modified: + + $> phpcbf --standard=PSR2 + +If the command-line tools `diff` and `patch` are unavailabe for you, `phpcbf` can use slightly slower PHP equivalents by using the `--no-patch` argument. + +###Code documentation + +If you are interested in having the documentation of the Friendica code outside of the code files, you can use [Doxygen](http://doxygen.org) to generate it. +The configuration file for Doxygen is located in the `util` directory of the project sources. +Run + + $> doxygen util/Doxyfile + +to generate the files which will be located in the `doc/html` subdirectory in the Friendica directory. +You can browse these files with any browser. + +If you find missing documentation, don't hesitate to contact us and write it down to enhance the code documentation. + ###Issues Have a look at our [issue tracker](https://github.com/friendica/friendica) on github! - * Try to reproduce a bug that needs more inquries and write down what you find out. + * Try to reproduce a bug that needs more inquiries and write down what you find out. * If a bug looks fixed, ask the bug reporters for feedback to find out if the bug can be closed. * Fix a bug if you can. Please make the pull request against the *develop* branch of the repository. * There is a *Junior Job* label for issues we think might be a good point to start with. @@ -66,15 +116,15 @@ If you want to get involved here: * Look at the first steps that were made (e.g. the clean theme). Ask us to find out whom to talk to about their experiences. * Talk to design people if you know any. -* Let us know about your plans [in the dev forum](https://friendika.openmindspace.org/profile/friendicadevelopers) and the [theme developer forum](https://friendica.eu/profile/ftdevs). +* Let us know about your plans [in the dev forum](https://helpers.pyxis.uberspace.de/profile/developers) and the [theme developer forum](https://friendica.eu/profile/ftdevs). Do not worry about cross-posting. ###Client software -There are free software clients that do somehow work with Friendica but most of them need love and maintenance. -Also, they were mostly made for other platforms using the GNU Social API. -This means they lack the features that are really specific to Friendica. -Popular clients you might want to have a look at are: +As Friendica is using a [Twitter/GNU Social compatible API](help/api) any of the clients for those platforms should work with Friendica as well. +Furthermore there are several client projects, especially for use with Friendica. +If you are interested in improving those clients, please contact the developers of the clients directly. -* [Hotot (Linux)](http://hotot.org/) - abandoned -* [Friendica for Android](https://github.com/max-weller/friendica-for-android) - abandoned -* You can find more working client software in [Wikipedia](https://en.wikipedia.org/wiki/Friendica). +* Android / CynogenMod: **Friendica for Android** [src](https://github.com/max-weller/friendica-for-android), [homepage](http://friendica.android.max-weller.de/) - abandoned +* iOS: *currently no client* +* SailfishOS: **Friendiy** [src](https://kirgroup.com/projects/fabrixxm/harbour-friendly) - developed by [Fabio](https://kirgroup.com/profile/fabrixxm/?tab=profile) +* Windows: **Friendica Mobile** for Windows versions [before 8.1](http://windowsphone.com/s?appid=e3257730-c9cf-4935-9620-5261e3505c67) and [Windows 10](https://www.microsoft.com/store/apps/9nblggh0fhmn) - developed by [Gerhard Seeber](http://mozartweg.dyndns.org/friendica/profile/gerhard/?tab=profile) diff --git a/doc/FAQ.md b/doc/FAQ.md index 0343833a25..36fe1a8b47 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -43,29 +43,29 @@ We recommend to talk to the admin(s) of the affected friendica server. (Admins, ###How can I upload images, files, links, videos and sound files to posts? -You can upload images from your computer by using the [editor](help/Text_editor). +You can upload images from your computer using the [editor](help/Text_editor). An overview of all uploaded images is listed at *yourpage.com/photos/profilename*. -On that page, you can also upload images directly and choose, if your contacts shall receive a message about this upload. +On that page, you can also upload images directly and choose if your contacts will receive a message about this upload. -Generally, you could attach every kind of file to a post. +Generally, you can attach any kind of file to a post. This is possible by using the "paper-clip"-symbol in the editor. These files will be linked to your post and can be downloaded by your contacts. -But it's not possible to get a preview for these ones. -Because of this, this upload method is recommended for office or zipped files. -If you want share content from Dropbox, Owncloud or any other [filehoster](http://en.wikipedia.org/wiki/Comparison_of_file_hosting_services), use the "link"-button (chain-symbol). +But it's not possible to get a preview for these items. +Because of this, this upload method is only recommended for office or zipped files. +If you want to share content from Dropbox, Owncloud or any other [filehoster](http://en.wikipedia.org/wiki/Comparison_of_file_hosting_services), use the "link"-button (chain-symbol). When you're adding URLs of other webpages with the "link"-button, Friendica tries to create a small preview. If this doesn't work, try to add the link by typing: [url=http://example.com]*self-chosen name*[/url]. You can also add video and audio files to posts. -But instead of a direct upload you have to use one of the following methods: +However, instead of a direct upload you have to use one of the following methods: -1. Add the video or audio link of a hoster (Youtube, Vimeo, Soundcloud and everyone else with oembed/opengraph-support). Videos will be shown with a preview image you can click on to start it. SoundCloud directly inserts a player to your post. +1. Add the video or audio link of a hoster (Youtube, Vimeo, Soundcloud and anyone else with oembed/opengraph-support). Videos will be shown with a preview image you can click on to start. SoundCloud directly inserts a player to your post. 2. If you have your own server, you can upload multimedia files via FTP and insert the URL. -Friendica is using HTML5 for embedding content. -Therefore, the supported files are depending on your browser and operating system. +Friendica uses HTML5 for embedding content. +Therefore, the supported files are dependent on your browser and operating system. Some supported filetypes are WebM, MP4, MP3 and OGG. See Wikipedia for more of them ([video](http://en.wikipedia.org/wiki/HTML5_video), [audio](http://en.wikipedia.org/wiki/HTML5_audio)). @@ -188,7 +188,7 @@ Admin ###Can I configure multiple domains with the same code instance? -No, this function is not supported anymore starting from Friendica 3.3. +No, this function is no longer supported from Friendica 3.3 onwards. @@ -202,12 +202,12 @@ Addons are listed at [this page](https://github.com/friendica/friendica-addons). If you are searching for new themes, you can find them at [Friendica-Themes.com](http://friendica-themes.com/) -###I've changed the my email address now the admin panel is gone? +###I've changed my email address now the admin panel is gone? Have a look into your .htconfig.php and fix your email address there. -###Can there be more then just one admin for a node? +###Can there be more then one admin for a node? Yes. You just have to list more then one email address in the .htconfig.php file. The listed emails need to be separated by a comma. diff --git a/doc/Forums.md b/doc/Forums.md index 2eac81a722..54e8e38791 100644 --- a/doc/Forums.md +++ b/doc/Forums.md @@ -53,9 +53,12 @@ Posting to Community forums If you are a member of a community forum, you may post to the forum by including an @-tag in the post mentioning the forum. For example @bicycle would send my post to all members of the group "bicycle" in addition to the normal recipients. -If your post is private you must also explicitly include the group in the post permissions (to allow the forum "contact" to see the post) **and** mention it in a tag (which redistributes the post to the forum members). +If you mention a forum (you are a member of) in a new posting, the posting will be distributed to all members of the forum, regardless of your privacy settings for the posting. +Also, if the forum is a public forum, your posting will be public for the all internet users. +If your post is private you must also explicitly include the group in the post permissions (to allow the forum "contact" to see the post) **and** mention it in a tag (which redistributes the post to the forum members). +Posting privately to a public forum, will result in your posting being displayed on the forum wall, but not on yours. -You may also post to a community forum by posting a "wall-to-wall" post using secure cross-site authentication. +You may also post to a community forum by posting a "wall-to-wall" post using secure cross-site authentication. Comments which are relayed to community forums will be relayed back to the original post creator. Mentioning the forum with an @-tag in a comment does not relay the message, as distribution is controlled entirely by the original post creator. diff --git a/doc/Home.md b/doc/Home.md index b37c76417c..b4b389921a 100644 --- a/doc/Home.md +++ b/doc/Home.md @@ -7,11 +7,11 @@ Friendica Documentation and Resources * [Account Basics](help/Account-Basics) * [New User Quick Start](help/Quick-Start-guide) * [Creating posts](help/Text_editor) - * [BBCode tag reference](help/BBCode) + * [BBCode tag reference](help/BBCode) * [Comment, sort and delete posts](help/Text_comment) * [Profiles](help/Profiles) * [Accesskey reference](help/Accesskeys) - * [Events](help/events) + * [Events](help/events) * You and other users * [Connectors](help/Connectors) * [Making Friends](help/Making-Friends) @@ -20,7 +20,6 @@ Friendica Documentation and Resources * [Community Forums](help/Forums) * [Chats](help/Chats) * Further information - * [Improve Performance](help/Improve-Performance) * [Move your account](help/Move-Account) * [Delete your account](help/Remove-Account) * [Frequently asked questions (FAQ)](help/FAQ) @@ -28,13 +27,12 @@ Friendica Documentation and Resources **Admin Manual** * [Install](help/Install) -* [Settings](help/Settings) +* [Settings & Admin Panel](help/Settings) * [Installing Connectors (Twitter/GNU Social)](help/Installing-Connectors) * [Install an ejabberd server (XMPP chat) with synchronized credentials](help/install-ejabberd) -* [Message Flow](help/Message-Flow) * [Using SSL with Friendica](help/SSL) -* [Twitter/GNU Social API Functions](help/api) * [Config values that can only be set in .htconfig.php](help/htconfig) +* [Improve Performance](help/Improve-Performance) **Developer Manual** @@ -46,7 +44,12 @@ Friendica Documentation and Resources * [Plugin Development](help/Plugins) * [Theme Development](help/themes) * [Smarty 3 Templates](help/smarty3-templates) +* [Protocol Documentation](help/Protocol) +* [Database schema documantation](help/database) +* [Class Autoloading](help/autoloader) * [Code - Reference(Doxygen generated - sets cookies)](doc/html/) +* [Twitter/GNU Social API Functions](help/api) + **External Resources** diff --git a/doc/Install.md b/doc/Install.md index 5afd5a22c1..9a194254a0 100644 --- a/doc/Install.md +++ b/doc/Install.md @@ -10,7 +10,7 @@ Not every PHP/MySQL hosting provider will be able to support Friendica. Many will. But **please** review the requirements and confirm these with your hosting provider prior to installation. -Also if you encounter installation issues, please let us know via the [helper](http://helpers.pyxis.uberspace.de/profile/helpers) or the [developer](https://friendika.openmindspace.org/profile/friendicadevelopers) forum or [file an issue](https://github.com/friendica/friendica/issues). +Also if you encounter installation issues, please let us know via the [helper](http://helpers.pyxis.uberspace.de/profile/helpers) or the [developer](https://helpers.pyxis.uberspace.de/profile/developers) forum or [file an issue](https://github.com/friendica/friendica/issues). Please be as clear as you can about your operating environment and provide as much detail as possible about any error messages you may see, so that we can prevent it from happening in the future. Due to the large variety of operating systems and PHP platforms in existence we may have only limited ability to debug your PHP installation or acquire any missing modules - but we will do our best to solve any general code issues. If you do not have a Friendica account yet, you can register a temporary one at [tryfriendica.de](https://tryfriendica.de) and join the forums mentioned above from there. @@ -26,12 +26,12 @@ Requirements --- * Apache with mod-rewrite enabled and "Options All" so you can use a local .htaccess file -* PHP 5.2+. The later the better. You'll need 5.3 for encryption of key exchange conversations. On a Windows environment, 5.2+ might not work as the function dns_get_record() is only available with version 5.3. +* PHP 5.4+. * PHP *command line* access with register_argc_argv set to true in the php.ini file * curl, gd, mysql, hash and openssl extensions * some form of email server or email gateway such that PHP mail() works * mcrypt (optional; used for server-to-server message encryption) -* Mysql 5.x or an equivalant alternative for MySQL (MariaDB etc.) +* Mysql 5.5.3+ or an equivalant alternative for MySQL (MariaDB, Percona Server etc.) * the ability to schedule jobs with cron (Linux/Mac) or Scheduled Tasks (Windows) (Note: other options are presented in Section 7 of this document.) * 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. @@ -69,6 +69,15 @@ Create an empty database and note the access details (hostname, username, passwo Friendica needs the permission to create and delete fields and tables in its own database. +With newer releases of MySQL (5.7.17 or newer), you might need to set the sql_mode to '' (blank). +Use this setting when the installer is unable to create all the needed tables due to a timestamp format problem. +In this case find the [mysqld] section in your my.cnf file and add the line : + + sql_mode = '' + +Restart mysql and you should be fine. + + ###Run the installer Point your web browser to the new site and follow the instructions. diff --git a/doc/Message-Flow.md b/doc/Message-Flow.md index ce0a4248ab..9a6785d599 100644 --- a/doc/Message-Flow.md +++ b/doc/Message-Flow.md @@ -4,7 +4,7 @@ Friendica Message Flow This page documents some of the details of how messages get from one person to another in the Friendica network. There are multiple paths, using multiple protocols and message formats. -Those attempting to understand these message flows should become familiar with (at the minimum) the [DFRN protocol document](http://dfrn.org/dfrn.pdf) and the message passing elements of the OStatus stack (salmon and Pubsubhubbub). +Those attempting to understand these message flows should become familiar with (at the minimum) the [DFRN protocol document](https://github.com/friendica/friendica/blob/master/spec/dfrn2.pdf) and the message passing elements of the OStatus stack (salmon and Pubsubhubbub). Most message passing involves the file include/items.php, which has functions for several feed-related import/export activities. @@ -21,8 +21,8 @@ Push (pubsubhubbub) feeds arrive via mod/pubsub.php DFRN-poll feed imports arrive via include/poller.php as a scheduled task, this implements the local side of the DFRN-poll protocol. -Scenario #1. Bob posts a public status message ---- +### Scenario #1. Bob posts a public status message + This is a public message with no conversation members so no private transport is used. There are two paths it can take - as a bbcode path to DFRN clients, and converted to HTML with the server's PuSH (pubsubhubbub) hubs notified. When a PuSH hub is operational, dfrn-poll clients prefer to receive their information through the PuSH channel. @@ -30,31 +30,31 @@ They will fall back on a daily poll in case the hub has delivery issues (this is If there is no specified hub or hubs, DFRN clients will poll at a configurable (per-contact) rate at up to 5-minute intervals. Feeds retrieved via dfrn-poll are bbcode and may also contain private conversations which the poller has permissions to see. -Scenario #2. Jack replies to Bob's public message. Jack is on the Friendica/DFRN network. ---- +### Scenario #2. Jack replies to Bob's public message. Jack is on the Friendica/DFRN network. + Jack uses dfrn-notify to send a direct reply to Bob. Bob then creates a feed of the conversation and sends it to everybody involved in the conversation using dfrn-notify. PuSH hubs are notified that new content is available. The hub or hubs will then retrieve the latest feed and transmit it to all hub subscribers (which may be on different networks). -Scenario #3. Mary replies to Bob's public message. Mary is on the Friendica/DFRN network. ---- +### Scenario #3. Mary replies to Bob's public message. Mary is on the Friendica/DFRN network. + Mary uses dfrn-notify to send a direct reply to Bob. Bob then creates a feed of the conversation and sends it to everybody involved in the conversation (excluding himself, the conversation is now sent to both Jack and Mary). Messages are sent using dfrn-notify. Push hubs are also notified that new content is available. The hub or hubs will then retrieve the latest feed and transmit it to all hub subscribers (which may be on different networks). -Scenario #4. William replies to Bob's public message. William is on the OStatus network. ---- +### Scenario #4. William replies to Bob's public message. William is on the OStatus network. + William uses salmon to notify Bob of the reply. Content is html embedded in salmon magic envelope. Bob then creates a feed of the conversation and sends it to all Friendica participants involved in the conversation using dfrn-notify (excluding himself, the conversation is sent to both Jack and Mary). Push hubs are notified that new content is available. The hub or hubs will then retrieve the latest feed and transmit it to all hub subscribers (which may be on different networks). -Scenario #5. Bob posts a private message to Mary and Jack. ---- +### Scenario #5. Bob posts a private message to Mary and Jack. + Message is delivered immediately to Mary and Jack using dfrn_notify. Public hubs are not notified. Requeueing is attempted in case of timeout. diff --git a/doc/Plugins.md b/doc/Plugins.md index 24d403e1f6..3a25dc7217 100644 --- a/doc/Plugins.md +++ b/doc/Plugins.md @@ -1,5 +1,7 @@ Friendica Addon/Plugin development -========================== +============== + +* [Home](help) Please see the sample addon 'randplace' for a working example of using some of these features. Addons work by intercepting event hooks - which must be registered. @@ -16,12 +18,12 @@ Future extensions may provide for "setup" amd "remove". Plugins should contain a comment block with the four following parameters: - /* - * Name: My Great Plugin - * Description: This is what my plugin does. It's really cool - * Version: 1.0 - * Author: John Q. Public - */ + /* + * Name: My Great Plugin + * Description: This is what my plugin does. It's really cool. + * Version: 1.0 + * Author: John Q. Public + */ Register your plugin hooks during installation. @@ -38,14 +40,14 @@ Arguments --- Your hook callback functions will be called with at least one and possibly two arguments - function myhook_function(&$a, &$b) { + function myhook_function(App $a, &$b) { } If you wish to make changes to the calling data, you must declare them as reference variables (with '&') during function declaration. -###$a +#### $a $a is the Friendica 'App' class. It contains a wealth of information about the current state of Friendica: @@ -56,13 +58,13 @@ It contains a wealth of information about the current state of Friendica: It is recommeded you call this '$a' to match its usage elsewhere. -###$b +#### $b $b can be called anything you like. This is information specific to the hook currently being processed, and generally contains information that is being immediately processed or acted on that you can use, display, or alter. Remember to declare it with '&' if you wish to alter it. Modules --------- +--- Plugins/addons may also act as "modules" and intercept all page requests for a given URL path. In order for a plugin to act as a module it needs to define a function "plugin_name_module()" which takes no arguments and needs not do anything. @@ -72,15 +74,15 @@ These are parsed into an array $a->argv, with a corresponding $a->argc indicatin So http://my.web.site/plugin/arg1/arg2 would look for a module named "plugin" and pass its module functions the $a App structure (which is available to many components). This will include: - $a->argc = 3 - $a->argv = array(0 => 'plugin', 1 => 'arg1', 2 => 'arg2'); + $a->argc = 3 + $a->argv = array(0 => 'plugin', 1 => 'arg1', 2 => 'arg2'); -Your module functions will often contain the function plugin_name_content(&$a), which defines and returns the page body content. -They may also contain plugin_name_post(&$a) which is called before the _content function and typically handles the results of POST forms. -You may also have plugin_name_init(&$a) which is called very early on and often does module initialisation. +Your module functions will often contain the function plugin_name_content(App $a), which defines and returns the page body content. +They may also contain plugin_name_post(App $a) which is called before the _content function and typically handles the results of POST forms. +You may also have plugin_name_init(App $a) which is called very early on and often does module initialisation. Templates ----------- +--- If your plugin needs some template, you can use the Friendica template system. Friendica uses [smarty3](http://www.smarty.net/) as a template engine. @@ -104,140 +106,140 @@ See also the wiki page [Quick Template Guide](https://github.com/friendica/frien Current hooks ------------- -###'authenticate' +### 'authenticate' 'authenticate' is called when a user attempts to login. $b is an array containing: - 'username' => the supplied username - 'password' => the supplied password + 'username' => the supplied username + 'password' => the supplied password 'authenticated' => set this to non-zero to authenticate the user. 'user_record' => successful authentication must also return a valid user record from the database -###'logged_in' +### 'logged_in' 'logged_in' is called after a user has successfully logged in. $b contains the $a->user array. -###'display_item' +### 'display_item' 'display_item' is called when formatting a post for display. $b is an array: 'item' => The item (array) details pulled from the database 'output' => the (string) HTML representation of this item prior to adding it to the page -###'post_local' +### 'post_local' * called when a status post or comment is entered on the local system * $b is the item array of the information to be stored in the database * Please note: body contents are bbcode - not HTML -###'post_local_end' +### 'post_local_end' * called when a local status post or comment has been stored on the local system * $b is the item array of the information which has just been stored in the database * Please note: body contents are bbcode - not HTML -###'post_remote' +### 'post_remote' * called when receiving a post from another source. This may also be used to post local activity or system generated messages. * $b is the item array of information to be stored in the database and the item body is bbcode. -###'settings_form' +### 'settings_form' * called when generating the HTML for the user Settings page * $b is the (string) HTML of the settings page before the final '' tag. -###'settings_post' +### 'settings_post' * called when the Settings pages are submitted * $b is the $_POST array -###'plugin_settings' +### 'plugin_settings' * called when generating the HTML for the addon settings page * $b is the (string) HTML of the addon settings page before the final '' tag. -###'plugin_settings_post' +### 'plugin_settings_post' * called when the Addon Settings pages are submitted * $b is the $_POST array -###'profile_post' +### 'profile_post' * called when posting a profile page * $b is the $_POST array -###'profile_edit' +### 'profile_edit' 'profile_edit' is called prior to output of profile edit page. $b is an array containing: 'profile' => profile (array) record from the database 'entry' => the (string) HTML of the generated entry -###'profile_advanced' +### 'profile_advanced' * called when the HTML is generated for the 'Advanced profile', corresponding to the 'Profile' tab within a person's profile page * $b is the (string) HTML representation of the generated profile * The profile array details are in $a->profile. -###'directory_item' +### 'directory_item' 'directory_item' is called from the Directory page when formatting an item for display. $b is an array: 'contact' => contact (array) record for the person from the database 'entry' => the (string) HTML of the generated entry -###'profile_sidebar_enter' +### 'profile_sidebar_enter' * called prior to generating the sidebar "short" profile for a page * $b is the person's profile array -###'profile_sidebar' +### 'profile_sidebar' 'profile_sidebar is called when generating the sidebar "short" profile for a page. $b is an array: 'profile' => profile (array) record for the person from the database 'entry' => the (string) HTML of the generated entry -###'contact_block_end' +### 'contact_block_end' is called when formatting the block of contacts/friends on a profile sidebar has completed. $b is an array: 'contacts' => array of contacts 'output' => the (string) generated HTML of the contact block -###'bbcode' +### 'bbcode' * called during conversion of bbcode to html * $b is a string converted text -###'html2bbcode' +### 'html2bbcode' * called during conversion of html to bbcode (e.g. remote message posting) * $b is a string converted text -###'page_header' +### 'page_header' * called after building the page navigation section * $b is a string HTML of nav region -###'personal_xrd' +### 'personal_xrd' 'personal_xrd' is called prior to output of personal XRD file. $b is an array: 'user' => the user record for the person 'xml' => the complete XML to be output -###'home_content' +### 'home_content' * called prior to output home page content, shown to unlogged users * $b is (string) HTML of section region -###'contact_edit' +### 'contact_edit' is called when editing contact details on an individual from the Contacts page. $b is an array: 'contact' => contact record (array) of target contact 'output' => the (string) generated HTML of the contact edit page -###'contact_edit_post' +### 'contact_edit_post' * called when posting the contact edit page. * $b is the $_POST array -###'init_1' +### 'init_1' * called just after DB has been opened and before session start * $b is not used or passed -###'page_end' +### 'page_end' * called after HTML content functions have completed * $b is (string) HTML of content div -###'avatar_lookup' +### 'avatar_lookup' 'avatar_lookup' is called when looking up the avatar. $b is an array: @@ -245,11 +247,11 @@ $b is an array: 'email' => email to look up the avatar for 'url' => the (string) generated URL of the avatar -###'emailer_send_prepare' +### 'emailer_send_prepare' 'emailer_send_prepare' called from Emailer::send() before building the mime message. $b is an array, params to Emailer::send() - 'fromName' => name of the sender + 'fromName' => name of the sender 'fromEmail' => email fo the sender 'replyTo' => replyTo address to direct responses 'toEmail' => destination email address @@ -258,20 +260,20 @@ $b is an array, params to Emailer::send() 'textVersion' => text only version of the message 'additionalMailHeader' => additions to the smtp mail header -###'emailer_send' +### 'emailer_send' is called before calling PHP's mail(). $b is an array, params to mail() - 'to' - 'subject' + 'to' + 'subject' 'body' 'headers' -###'nav_info' +### 'nav_info' is called after the navigational menu is build in include/nav.php. $b is an array containing $nav from nav.php. -###'template_vars' +### 'template_vars' is called before vars are passed to the template engine to render the page. The registered function can add,change or remove variables passed to template. $b is an array with: @@ -279,6 +281,11 @@ $b is an array with: 'template' => filename of template 'vars' => array of vars passed to template +### ''acl_lookup_end' +is called after the other queries have passed. +The registered function can add, change or remove the acl_lookup() variables. + + 'results' => array of the acl_lookup() vars Complete list of hook callbacks @@ -336,6 +343,8 @@ include/acl_selectors.php: call_hooks($a->module . '_pre_' . $selname, $arr); include/acl_selectors.php: call_hooks($a->module . '_post_' . $selname, $o); +include/acl_selectors.php call_hooks('acl_lookup_end', $results); + include/notifier.php: call_hooks('notifier_normal',$target_item); include/notifier.php: call_hooks('notifier_end',$target_item); @@ -463,4 +472,3 @@ mod/cb.php: call_hooks('cb_afterpost'); mod/cb.php: call_hooks('cb_content', $o); mod/directory.php: call_hooks('directory_item', $arr); - diff --git a/doc/Protocol.md b/doc/Protocol.md new file mode 100644 index 0000000000..85fe09a5f1 --- /dev/null +++ b/doc/Protocol.md @@ -0,0 +1,42 @@ +Used Protocols +=============== + +* [Home](help) + +Friendicas DFRN Protocol +--- + +* [Document with the DFRN specification](spec/dfrn2.pdf) +* [Schema of the contact request process](spec/dfrn2_contact_request.png) +* [Schema of the contact request confirmation](spec/dfrn2_contact_confirmation.png) +* [Description of the message flow](help/Message-Flow) + +ActivityStreams +--- + +Friendica is using ActivityStreams in version 1.0 for its activities and object types. +Additional types are used for non standard activities. + +* [Link to the specification](http://activitystrea.ms/head/activity-schema.html) +* [List of used ActivityStreams verbs and object types.](https://github.com/friendica/friendica/wiki/ActivityStreams) + +Salmon +--- + +Salmon is used as a message exchange protocol for replies and mentions. + +* [Link to the protocol summary](http://www.salmon-protocol.org/salmon-protocol-summary) + +Portable Contacts +--- + +Portable Contacts is used for friends lists. + +* [Link to the specification](https://web.archive.org/web/20160426223008/http://portablecontacts.net/draft-spec.html) (Link to archive.org) + +pubsubhubbub +--- + +pubsubhubbub is used for OStatus. + +* [Link to the specification](https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html) diff --git a/doc/Quick-Start-andfinally.md b/doc/Quick-Start-andfinally.md index 97afd8140c..e76eb7278f 100644 --- a/doc/Quick-Start-andfinally.md +++ b/doc/Quick-Start-andfinally.md @@ -5,17 +5,8 @@ Here are some more things to help get you started: **Groups** -- New Here - a group for people new to Friendica - - Friendica Support - problems? This is the place to ask. -- Public Stream - a place to talk about anything to anyone. - -- Let's Talk a group for finding people and groups who share similar interests. - -- Local Friendica a page for local Friendica groups - - **Documentation** - Connecting to more networks diff --git a/doc/SSL.md b/doc/SSL.md index a72eec2a16..95de83305f 100644 --- a/doc/SSL.md +++ b/doc/SSL.md @@ -3,15 +3,15 @@ Using SSL with Friendica * [Home](help) -Disclaimer ---- -**This document has been updated in November 2015. +## Disclaimer + +**This document has been updated in November 2016. SSL encryption is relevant for security. This means that recommended settings change fast. Keep your setup up to date and do not rely on this document being updated as fast as technologies change!** -Intro ---- +## Intro + If you are running your own Friendica site, you may want to use SSL (https) to encrypt communication between servers and between yourself and your server. There are basically two sorts of SSL certificates: Self-signed certificates and certificates signed by a certificate authority (CA). @@ -26,81 +26,73 @@ Normally, you have to pay for them - and they are valid for a limited period of There are ways to get a trusted certificate for free. -Chose your domain name ---- +## Choose your domain name Your SSL certificate will be valid for a domain or even only for a subdomain. Make your final decision about your domain resp. subdomain *before* ordering the certificate. Once you have it, changing the domain name means getting a new certificate. -Shared hosts ---- +### Shared hosts If your Friendica instance is running on a shared hosting platform, you should first check with your hosting provider. They have instructions for you on how to do it there. You can always order a paid certificate with your provider. They will either install it for you or provide an easy way to upload the certificate and the key via a web interface. - - -It might be worth asking if your provider would install a certificate you provide yourself, to save money. -If so, read on. - -Getting a free StartSSL certificate ---- -StartSSL is a certificate authority that issues certificates for free. -They are valid for a year and are sufficient for our purposes. - -### Step 1: Create a client certificate - -When you initially sign up with StartSSL, you receive a certificate that is installed in your browser. -You need it for the login on startssl.com, also when coming back to the site later. -It has nothing to do with the SSL certificate for your server. - -### Step 2: Validate your email address and your domain - -To continue you have to prove that you own the email address you specified and the domain that you want a certificate for. -Specify your email address, request a validation link via email from the "validations wizard". -Same procedure for the domain validation. - -### Step 3: Request the certificate - -Go to the "certificates wizard". -Choose the target web server. -When you are first prompted for a domain to certify, you need to enter your main domain, e.g. example.com. -In the next step, you will be able to specify a subdomain for Friendica, if needed. -Example: If you have friendica.example.com, you first enter example.com, then specify the subdomain friendica later. - -If you know how to generate an openssl key and a certificate signing request (csr) yourself, do so. -Paste the csr into your browser to get it signed by StartSSL. - -If you do not know how to generate a key and a csr, accept StartSSL's offer to generate it for you. -This means: StartSSL has the key to your encryption but it is better than no certificate at all. -Download your certificate from the website. -(Or in the second case: Download your certificate and your key.) - -To install your certificate on a server, you need one or two extra files: sub.class1.server.ca.pem and ca.pem, delivered by startssl.com -Go to the "Tool box" section and download "Class 1 Intermediate Server CA" and "StartCom Root CA (PEM encoded)". - -If you want to send your certificate to your hosting provider, they need the certificate, the key and probably at least the intermediate server CA. -To be sure, send those three and the ca.pem file. +With some providers, you have to send them your certificate. +They need the certificate, the key and the CA's intermediate certificate. +To be sure, send those three files. **You should send them to your provider via an encrypted channel!** -If you run your own server, upload the files and check out the Mozilla wiki link below. +### Own server -Let's encrypt ---- +If you run your own server, we recommend to check out the ["Let's Encrypt" initiative](https://letsencrypt.org/). +Not only do they offer free SSL certificates, but also a way to automate their renewal. +You need to install a client software on your server to use it. +Instructions for the official client are [here](https://certbot.eff.org/). +Depending on your needs, you might want to look at the [list of alternative letsencrypt clients](https://letsencrypt.org/docs/client-options/). -If you run your own server and you control your name server, the "Let's encrypt" initiative might become an interesting alternative. -Their offer is not ready, yet. -Check out [their website](https://letsencrypt.org/) for status updates. - -Web server settings ---- +## Web server settings Visit the [Mozilla's wiki](https://wiki.mozilla.org/Security/Server_Side_TLS) for instructions on how to configure a secure webserver. -They provide recommendations for [different web servers](https://wiki.mozilla.org/Security/Server_Side_TLS#Recommended_Server_Configurations). +They provide recommendations for [different web servers](https://mozilla.github.io/server-side-tls/ssl-config-generator/). -Test your SSL settings ---- +## Test your SSL settings When you are done, visit the test site [SSL Labs](https://www.ssllabs.com/ssltest/) to have them check if you succeeded. + +## Configure Friendica + +If you can successfully access your Friendica instance through https, there are a number of steps you can take to ensure your users will use SSL to access your instance. + +### Web server redirection + +This is the simplest way to enforce site-wide secure access. +Every time a user tries to access any Friendica page by any mean (manual address bar entry or link), the web server issues a Permanent Redirect response with the secure protocol prepended to the requested URL. + +With Apache, simply add the following lines to the [code].htaccess[/code] file in the root folder of your Friendica instance (thanks to [url=https://github.com/AlfredSK]AlfredSK[/url]): + +[code] +#Force SSL connections + +RewriteEngine On +RewriteCond %{SERVER_PORT} 80 +RewriteRule ^(.*)$ https://your.friendica.domain/$1 [R=301,L] +[/code] + +With nginx, configure your [code]server[/code] directive this way (thanks to [url=https://bjornjohansen.no/redirect-to-https-with-nginx/]Bjørn Johansen[/url]): + +[code] +server { + listen 80; + listen [::]:80; + server_name your.friendica.domain; + return 301 https://$server_name$request_uri; +} +[/code] + +### SSL Settings + +In the Admin Settings, there are three SSL-related settings: +- **SSL link policy**: this affects how Friendica generates internal links. If your SSL installation was successful, we recommend "Force all links to SSL" just in case your web server configuration can't be altered like described above. +- **Force SSL**: This forces all external links to HTTPS, which may solve Mixed-Content issues, but not all websites support HTTPS yet. Use at your own risk. +- **Verify SSL**: Enabling this will prevent Friendica to interact with self-signed SSL sites. We recommend you leave it on as a self-signed SSL certificate can be a vectorfor a man-in-the-middle attack. \ No newline at end of file diff --git a/doc/Settings.md b/doc/Settings.md index ae7d916078..9590ad42d6 100644 --- a/doc/Settings.md +++ b/doc/Settings.md @@ -1,175 +1,108 @@ -Settings -=== -Here are some of the built-in features which don't have an exposed interface or are otherwise undocumented. -Configuration settings are stored in the file ".htconfig.php". -Edit this file with a text editor to make the desired changes. -Several system settings are already documented in that file and will not be covered here. +# Settings -Hot Keys ---- +* [Home](help) -Friendica traps the following keyboard events: +If you are the admin of a Friendica node, you have access to the so called **Admin Panel** where you can configure your Friendica node. -* [Pause] - Pauses "Ajax" update activity. This is the process that provides updates without reloading the page. You may wish to pause it to reduce network usage and/or as a debugging aid for javascript developers. A pause indicator will appear at the lower right hand corner of the page. Hit the [pause] key once again to resume. -* [F8] - Displays a language selector +On the front page of the admin panel you will see a summary of information about your node. +These information include the amount of messages currently being processed in the queues. +The first number is the number of messages being actively sent. +This number should decrease quickly. +The second is the messages which could for various reasons not being delivered. +They will be resend later. +You can have a quick glance into that second queus in the "Inspect Queue" section of the admin panel. +If you have activated the background workers, there is a third number representing the count of jobs queued for the workers. +These worker tasks are prioritised and are done accordingly. +Then you get an overview of the accounts on your node, which can be moderated in the "Users" section of the panel. +As well as an overview of the currently active addons +The list is linked, so you can have quick access to the plugin settings. +And finally you are informed about the version of Friendica you have installed. +If you contact the devs with a bug or problem, please also mention the version of your node. -Birthday Notifications ---- +The admin panel is seperated into subsections accessible from the side bar of the panel. -Birthday events are published on your Home page for any friends having a birthday in the coming 6 days. -In order for your birthday to be discoverable by all of your friends, you must set your birthday (at least the month and day) in your default profile. -You are not required to provide the year. +## Site -System settings ---- +This section of the admin panel contains the main configuration of your Friendica node. +It is separated into several sub-section beginning with the basic settings at the top, advancing towards the bottom of the page. -###Language +Most configuration options have a help text in the admin panel. +Therefore this document does not yet cover all the options -Please see util/README for information on creating language translations. +### Basic Settings -Config: - - $a->config['system']['language'] = 'name'; - -###System Theme - -Choose a theme to be the default system theme. This can be over-ridden by user profiles. -Default theme is "default". - -Config: - - $a->config['system']['theme'] = 'theme-name'; - -###Proxy Configuration Settings - -If your site uses a proxy to connect to the internet, you may use these settings to communicate with the outside world. -The outside world still needs to be able to see your website, or this will not be very useful. - -Config: - - $a->config['system']['proxy'] = "http://proxyserver.domain:port"; - $a->config['system']['proxyuser'] = "username:password"; - -###Network Timeout - -How long to wait on a network communication before timing out. -Value is in seconds. -Default is 60 seconds. -Set to 0 for unlimited (not recommended). - -Config: - - $a->config['system']['curl_timeout'] = 60; - -###Banner/Logo +#### Banner/Logo Set the content for the site banner. The default logo is the Friendica logo and name. -You may wish to provide HTML/CSS to style and/or position this content, as it may not be themed by default. +You may wish to provide HTML/CSS to style and/or position this content, as it may not be themed by default. -Config: +#### Language - $a->config['system']['banner'] = 'My Great Website'; +This option will set the default language for the node. +It is used as fall back setting should Friendica fail to recognize the visitors preferences and can be overwritten by user settings. -###Maximum Image Size +The Friendica community offers some translations. +Some more compleate then others. +See [this help page](/help/translations) for more information about the translation process. -Maximum size in bytes of uploaded images. -The default is set to 0, which means no limits. +#### System Theme -Config: +Choose a theme to be the default system theme. +This can be over-ridden by user profiles. +Default theme is "duepunto zero" at the moment. - $a->config['system']['maximagesize'] = 1000000; +You may also want to set a special theme for mobile interfaces. +Which may or may not be neccessary depending of the mobile friendlyness of the desktop theme you have chosen. +The `vier` theme for instance is mobile friendly. -###UTF-8 Regular Expressions +### Registration -During registrations, full names are checked using UTF-8 regular expressions. -This requires PHP to have been compiled with a special setting to allow UTF-8 expressions. -If you are completely unable to register accounts, set no_utf to true. -The default is set to false (meaning UTF8 regular expressions are supported and working). - -Config: - - $a->config['system']['no_utf'] = true; - -###Check Full Names +#### Check Full Names You may find a lot of spammers trying to register on your site. During testing we discovered that since these registrations were automatic, the "Full Name" field was often set to just an account name with no space between first and last name. If you would like to support people with only one name as their full name, you may change this setting to true. Default is false. - -Config: - $a->config['system']['no_regfullname'] = true; - -###OpenID +#### OpenID By default, OpenID may be used for both registration and logins. If you do not wish to make OpenID facilities available on your system (at all), set 'no_openid' to true. Default is false. -Config: - - $a->config['system']['no_openid'] = true; - -###Multiple Registrations +#### Multiple Registrations The ability to create "Pages" requires a person to register more than once. Your site configuration can block registration (or require approval to register). By default, logged in users can register additional accounts for use as pages. -These will still require approval if REGISTER_APPROVE is selected. -You may prohibit logged in users from creating additional accounts by setting 'block_extended_register' to true. +These will still require approval if the registration policy is set to *require approval* +You may prohibit logged in users from creating additional accounts by setting *block multible registrations* to true. Default is false. - -Config: - $a->config['system']['block_extended_register'] = true; +### File upload -Security settings ---- +#### Maximum Image Size -###Verify SSL Certitificates +Maximum size in bytes of uploaded images. +The default is set to 0, which means no limits. -By default Friendica allows SSL communication between websites that have "self-signed" SSL certificates. -For the widest compatibility with browsers and other networks we do not recommend using self-signed certificates, but we will not prevent you from using them. -SSL encrypts all the data transmitted between sites (and to your browser). This allows you to have completely encrypted communications, and also protect your login session from hijacking. -Self-signed certificates can be generated for free, without paying top-dollar for a website SSL certificate. -However these aren't looked upon favourably in the security community because they can be subject to so-called "man-in-the-middle" attacks. -If you wish, you can turn on strict certificate checking. -This will mean you cannot connect (at all) to self-signed SSL sites. +### Policies -Config: +#### Global Directory - $a->config['system']['verifyssl'] = true; +This configures the URL to update the global directory, and is supplied in the default configuration. +The undocumented part is that if this is not set, the global directory is completely unavailable to the application. +This allows a private community to be completely isolated from the global network. -Corporate/Edu enhancements ---- +#### Force Publish -###Allowed Friend Domains +By default, each user can choose on their Settings page whether or not to have their profile published in the site directory. +This setting forces all profiles on this site to be listed in the site directory and there is no option provided to the user to change it. +Default is false. -Comma separated list of domains which are allowed to establish friendships with this site. -Wildcards are accepted. -(Wildcard support on Windows platforms requires PHP5.3). -By default, any (valid) domain may establish friendships with this site. - -Config: - - $a->config['system']['allowed_sites'] = "sitea.com, *siteb.com"; - -###Allowed Email Domains - -Comma separated list of domains which are allowed in email addresses for registrations to this site. -This can lockout those who are not part of this organisation from registering here. -Wildcards are accepted. -(Wildcard support on Windows platforms requires PHP5.3). -By default, any (valid) email address is allowed in registrations. - -Config: - - $a->config['system']['allowed_email'] = "sitea.com, *siteb.com"; - -###Block Public +#### Block Public Set to true to block public access to all otherwise public personal pages on this site unless you are currently logged in. This blocks the viewing of profiles, friends, photos, the site directory and search pages to unauthorised persons. @@ -179,50 +112,164 @@ Note: this is specifically for sites that desire to be "standalone" and do not w Unauthorised persons will also not be able to request friendship with site members. Default is false. Available in version 2.2 or greater. - -Config: - $a->config['system']['block_public'] = true; +#### Allowed Friend Domains -###Force Publish +Comma separated list of domains which are allowed to establish friendships with this site. +Wildcards are accepted. +(Wildcard support on Windows platforms requires PHP5.3). +By default, any (valid) domain may establish friendships with this site. -By default, each user can choose on their Settings page whether or not to have their profile published in the site directory. -This setting forces all profiles on this site to be listed in the site directory and there is no option provided to the user to change it. -Default is false. - -Config: +This is useful if you want to setup a closed network for educational groups, cooperations and similar communities that don't want to commuicate with the rest of the network. - $a->config['system']['publish_all'] = true; +#### Allowed Email Domains -###Global Directory +Comma separated list of domains which are allowed in email addresses for registrations to this site. +This can lockout those who are not part of this organisation from registering here. +Wildcards are accepted. +(Wildcard support on Windows platforms requires PHP5.3). +By default, any (valid) email address is allowed in registrations. -This configures the URL to update the global directory, and is supplied in the default configuration. -The undocumented part is that if this is not set, the global directory is completely unavailable to the application. -This allows a private community to be completely isolated from the global network. +#### Allow Users to set remote_self - $a->config['system']['directory'] = 'http://dir.friendi.ca'; +If you enable the `Allow Users to set remote_self` users can select Atom feeds from their contact list being their *remote self* in die advanced contact settings. +Which means that postings by the remote self are automatically reposted by Friendica in their names. -Developer Settings ---- +As admin of the node you can also set this flag directly in the database. +Before doing so, you should be sure you know what you do and have a backup of the database. -### Debugging -Most useful when debugging protocol exchanges and tracking down other communications issues. +### Advanced -Config: +#### Proxy Configuration Settings - $a->config['system']['debugging'] = true; - $a->config['system']['logfile'] = 'logfile.out'; - $a->config['system']['loglevel'] = LOGGER_DEBUG; +If your site uses a proxy to connect to the internet, you may use these settings to communicate with the outside world. +The outside world still needs to be able to see your website, or this will not be very useful. -Turns on detailed debugging logs which will be stored in 'logfile.out' (which must be writeable by the webserver). -LOGGER_DEBUG will show a good deal of information about system activity but will not include detailed data. -You may also select LOGGER_ALL but due to the volume of information we recommend only enabling this when you are tracking down a specific problem. -Other log levels are possible but are not being used at the present time. +#### Network Timeout +How long to wait on a network communication before timing out. +Value is in seconds. +Default is 60 seconds. +Set to 0 for unlimited (not recommended). -###PHP error logging +#### UTF-8 Regular Expressions -Use the following settings to redirect PHP errors to a file. +During registrations, full names are checked using UTF-8 regular expressions. +This requires PHP to have been compiled with a special setting to allow UTF-8 expressions. +If you are completely unable to register accounts, set no_utf to true. +The default is set to false (meaning UTF8 regular expressions are supported and working). + +#### Verify SSL Certitificates + +By default Friendica allows SSL communication between websites that have "self-signed" SSL certificates. +For the widest compatibility with browsers and other networks we do not recommend using self-signed certificates, but we will not prevent you from using them. +SSL encrypts all the data transmitted between sites (and to your browser). +This allows you to have completely encrypted communications, and also protect your login session from hijacking. +Self-signed certificates can be generated for free, without paying top-dollar for a website SSL certificate. +However these aren't looked upon favourably in the security community because they can be subject to so-called "man-in-the-middle" attacks. +If you wish, you can turn on strict certificate checking. +This will mean you cannot connect (at all) to self-signed SSL sites. + +### Auto Discovered Contact Directory + +### Performance + +### Worker + +### Relocate + +## Users + +This section of the panel let the admin control the users registered on the node. + +If you have selected "Requires approval" for the *Register policy* in the general nodes configuration, new registrations will be listed at the top of the page. +There the admin can then approve or disapprove the request. + +Below the new registration block the current accounts on the Friendica node are listed. +You can sort the user list by name, email, registration date, date of last login, date of last posting and the account type. +Here the admin can also block/unblock users from accessing the node or delete the accounts entirely. + +In the last section of the page admins can create new accounts on the node. +The password for the new account will be send by email to the choosen email address. + +## Plugins + +This page is for selecting and configuration of extensions for Friendica which have to be placed into the `/addon` subdirectory of your Friendica installation. +You are presented with a long list of available addons. +The name of each addon is linked to a separate page for that addon which offers more informations and configuration possibilities. +Also shown is the version of the addon and an indicator if the addon is currently active or not. + +When you update your node and the addons they may have to be reloaded. +To simplify this process there is a button at the top of the page to reload all active plugins. + +## Themes + +The Themes section of the admin panel works similar to the Plugins section but let you control the themes on your Friendica node. +Each theme has a dedicated suppage showing the current status, some information about the theme and a screen-shot of the Friendica interface using the theme. +Should the theme offer special settings, admins can set a global default value here. + +You can activate and deactivate themes on their dedicated sub-pages thus making them available for the users of the node. +To select a default theme for the Friendica node, see the *Site* section of the admin panel. + +## Additional Features + +There are several optional features in Friendica like the *dislike* button. +In this section of the admin panel you can select a default setting for your node and eventually fix it, so users cannot change the setting anymore. + +## DB Updates + +Should the database structure of Friendica change, it will apply the changes automatically. +In case you are suspecious that the update might not have worked, you can use this section of the admin panel to check the situation. + +## Inspect Queue + +In the admin panel summary there are two numbers for the message queues. +The second number represents messages which could not be delivered and are queued for later retry. +If this number goes sky-rocking you might ask yourself which receopiant is not receiving. + +Behind the inspect queue section of the admin panel you will find a list of the messages that could not be delivered. +The listing is sorted by the receipiant name so identifying potential broken communication lines should be simple. +These lines might be broken for various reasons. +The receiving end might be off-line, there might be a high system load and so on. + +Don't panic! +Friendica will not queue messages for all time but will sort out *dead* nodes automatically after a while and remove messages from the queue then. + +## Federation Statistics + +The federation statistics page gives you a short summery of the nodes/servers/pods of the decentralized social network federation your node knows. +These numbers are not compleate and only contain nodes from networks Friendica federates directly with. + +## Plugin Features + +Some of the addons you can install for your Friendica node have settings which have to be set by the admin. +All those addons will be listed in this area of the admin panels side bar with their names. + +## Logs + +The log section of the admin panel is seperated into two pages. +On the first, following the "log" link, you can configure how much Friendica shall log. +And on the second you can read the log. + +You should not place your logs into any directory that is accessible from the web. +If you have to, and you are using the default configuration from Apache, you should choose a name for the logfile ending in ``.log`` or ``.out``. +Should you use another web server, please make sure that you have the correct accessrules in place so that your log files are not accessible. + +There are five different log levels: Normal, Trace, Debug, Data and All. +Specifying different verbosities of information and data written out to the log file. +Normally you should not need to log at all. +The *DEBUG* level will show a good deal of information about system activity but will not include detailed data. +In the *ALL* level Friendica will log everything to the file. +But due to the volume of information we recommend only enabling this when you are tracking down a specific problem. + +**The amount of data can grow the filesize of the logfile quickly**. +You should set up some kind of [log rotation](https://en.wikipedia.org/wiki/Log_rotation) to keep the log file from growing too big. + +**Known Issues**: The filename ``friendica.log`` can cause problems depending on your server configuration (see [issue 2209](https://github.com/friendica/friendica/issues/2209)). + +By default PHP warnings and error messages are supressed. +If you want to enable those, you have to activate them in the ``.htconfig.php`` file. +Use the following settings to redirect PHP errors to a file. Config: @@ -232,9 +279,61 @@ Config: ini_set('display_errors', '0'); This will put all PHP errors in the file php.out (which must be writeable by the webserver). -Undeclared variables are occasionally referenced in the program and therefore we do not recommend using E_NOTICE or E_ALL. +Undeclared variables are occasionally referenced in the program and therefore we do not recommend using `E_NOTICE` or `E_ALL`. The vast majority of issues reported at these levels are completely harmless. Please report to the developers any errors you encounter in the logs using the recommended settings above. -They generally indicate issues which need to be resolved. +They generally indicate issues which need to be resolved. + +If you encounter a blank (white) page when using the application, view the PHP logs - as this almost always indicates an error has occurred. + +## Diagnostics + +In this section of the admin panel you find two tools to investigate what Friendica sees for certain ressources. +These tools can help to clarify communication problems. + +For the *probe address* Friendica will display information for the address provided. + +With the second tool *check webfinger* you can request information about the thing identified by a webfinger (`someone@example.com`). + +# Exceptions to the rule + +There are four exceptions to the rule, that all the config will be read from the data base. +These are the data base settings, the admin account settings, the path of PHP and information about an eventual installation of the node in a sub-directory of the (sub)domain. + +## DB Settings + +With the following settings, you specify the data base server, the username and passwort for Friendica and the database to use. + + $db_host = 'your.db.host'; + $db_user = 'db_username'; + $db_pass = 'db_password'; + $db_data = 'database_name'; + +## Admin users + +You can set one, or more, accounts to be *Admin*. +By default this will be the one account you create during the installation process. +But you can expand the list of email addresses by any used email address you want. +Registration of new accounts with a listed email address is not possible. + + $a->config['admin_email'] = 'you@example.com, buddy@example.com'; + +## PHP Path + +Some of Friendicas processes are running in the background. +For this you need to specify the path to the PHP binary to be used. + + $a->config['php_path'] = '{{$phpath}}'; + +## Subdirectory configuration + +It is possible to install Friendica into a subdirectory of your webserver. +We strongly discurage you from doing so, as this will break federation to other networks (e.g. Diaspora, GNU Socia, Hubzilla) +Say you have a subdirectory for tests and put Friendica into a further subdirectory, the config would be: + + $a->path = 'tests/friendica'; + +## Other exceptions + +Furthermore there are some experimental settings, you can read-up in the [Config values that can only be set in .htconfig.php](help/htconfig) section of the documentation. -If you encounter a blank (white) page when using the application, view the PHP logs - as this almost always indicates an error has occurred. diff --git a/doc/Tags-and-Mentions.md b/doc/Tags-and-Mentions.md index 5501d0d580..eac2c97f84 100644 --- a/doc/Tags-and-Mentions.md +++ b/doc/Tags-and-Mentions.md @@ -21,11 +21,22 @@ You can tag a person on a different network or one that is **not in your social * @mike@macgirvin.com - This is called a "remote mention" and can only be an email-style locator, not a web URL. -Unless their system blocks unsolicited "mentions", the person tagged will likely receive a "Mention" post/activity or become a direct participant in the conversation in the case of public posts. Please note that Friendica blocks incoming "mentions" from people with no relationship to you. This is a spam prevention measure. +Unless their system blocks unsolicited "mentions", the person tagged will likely receive a "Mention" post/activity or become a direct participant in the conversation in the case of public posts. +Friendica blocks incoming “mentions” from people with no relationship to you. +The exception is an ongiong conversation started from a contact of both you and the 3rd person or a conversation in a forum where you are a member of. +This is a spam prevention measure. -Remote mentions are delivered using the OStatus protocol. This protocol is used by Friendica and GNU Social and several other systems, but is not currently implemented in Diaspora. +Remote mentions are delivered using the OStatus protocol. +This protocol is used by Friendica and GNU Social and several other systems, but is not currently implemented in Diaspora. +As the OStatus protocol allows this Friendica user can be @-mentioned by users from platforms using this protocol in conversations if the "Enable OStatus support" is activated on the Friendica node. +These @-mentions wont be blocked, even if there is no relationship between the sender and the receiver of the message. -Friendica makes no distinction between people and groups for the purpose of tagging. (Some other networks use !group to indicate a group.) +Friendica makes no distinction between people and forums for the purpose of tagging. +(Some other networks use !forum to indicate a forum.) + +If you sort your contacts into groups, you cannot @-mention these groups. +But you can select the group in the access control when creating a new posting, to allow (or disallow) a certain group of people to see the posting. +See [Groups and Privacy](help/Groups-and-Privacy) for more details about grouping your contacts. **Topical Tags** diff --git a/doc/Text_editor.md b/doc/Text_editor.md index 2d38217674..3e38ca5f1e 100644 --- a/doc/Text_editor.md +++ b/doc/Text_editor.md @@ -1,7 +1,7 @@ - Creating posts @@ -9,7 +9,7 @@ Creating posts * [Home](help) -Here you can find an overview of the different ways to create and edit your post. +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: @@ -42,7 +42,7 @@ The icons under the text area are there to help you to write posts quickly: 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.*

-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.

@@ -88,26 +88,11 @@ Click on "show" under contact name to hide the post to everyone but selected. Click on "Visible to everybody" to make the post public again. -If you have defined some groups, you can check "show" for groups also. All contact in that group will see the post. +If you have defined some groups, you can check "show" for groups also. All contact in that group will see the post. If you want to hide the post to one contact of a group selected for "show", click "don't show" under contact name. 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) - - - -WYSIAWYG (What You See Is About What You Get) --------------------------------------------------- - -Friendica can use TinyMCE as rich text editor. This way you can write beatifull post without the need to know [BBCode](help/BBCode). - -By default, rich editor is disabled. You can enable it from Settings -> [Aditional features](settings/features) page, turn on Richtext Editor and click "Submit". - -
-default editor -
Rich editor, with default Friendica theme (duepuntozero)
-
- +See also [Group and Privacy](help/Groups-and-Privacy) \ No newline at end of file diff --git a/doc/Vagrant.md b/doc/Vagrant.md index 4bc9e6c54d..ec706e5c38 100644 --- a/doc/Vagrant.md +++ b/doc/Vagrant.md @@ -8,7 +8,11 @@ Getting started [Vagrant](https://www.vagrantup.com/) is a virtualization solution for developers. No need to setup up a webserver, database etc. before actually starting. -Vagrant creates a virtual machine (an Ubuntu 14.04) for you that you can just run inside VirtualBox and start to work directly on Friendica. +Vagrant creates a virtual machine for you that you can just run inside VirtualBox and start to work directly on Friendica. +You can choose between two different Ubuntu Linux versions: + +1. Ubuntu Trusty (14.04) with PHP 5.5.9 and MySQL 5.5.53 +2. Ubuntu Xenial (16.04) with PHP 7.0 and MySQL 5.7.16 What you need to do: @@ -16,21 +20,27 @@ What you need to do: Please use an up-to-date vagrant version from https://www.vagrantup.com/downloads.html. 2. Git clone your Friendica repository. Inside, you'll find a "Vagrantfile" and some scripts in the utils folder. -3. Run "vagrant up" from inside the friendica clone. +3. Choose the Ubuntu version you'll need und run "vagrant up " from inside the friendica clone: + $> vagrant up trusty + $> vagrant up xenial Be patient: When it runs for the first time, it downloads an Ubuntu Server image. -4. Run "vagrant ssh" to log into the virtual machine to log in to the VM. -5. Open 192.168.22.10 in a browser. +4. Run "vagrant ssh " to log into the virtual machine to log in to the VM: + $> vagrant ssh trusty + $> vagrant ssh xenial +5. Open you test installation in a browser. +If you selected an Ubuntu Trusty go to 192.168.22.10. +If you started a Xenial machine go to 192.168.22.11. The mysql database is called "friendica", the mysql user and password both are "root". 6. Work on Friendica's code in your git clone on your machine (not in the VM). Your local working directory is set up as a shared directory with the VM (/vagrant). 7. Check the changes in your browser in the VM. -Debug via the "vagrant ssh" login. +Debug via the "vagrant ssh " login. Find the Friendica log file /vagrant/logfile.out. 8. Commit and push your changes directly back to Github. If you want to stop vagrant after finishing your work, run the following command - $> vagrant halt + $> vagrant halt in the development directory. @@ -44,10 +54,3 @@ You will then have the following accounts to login: * friendica2 and friendica3 are conntected. friendica4 and friendica5 are connected. For further documentation of vagrant, please see [the vagrant*docs*](https://docs.vagrantup.com/v2/). - -**Important notice:** -If you already had an Ubuntu 12.04 Vagrant VM, please run - - $> vagrant destroy - -before starting the new 14.04 machine. diff --git a/doc/api.md b/doc/api.md index 147c8b7513..b759b4697c 100644 --- a/doc/api.md +++ b/doc/api.md @@ -1,12 +1,30 @@ -Implemented API calls +Friendica API === -The Friendica API aims to be compatible to the [GNU Social API](http://skilledtests.com/wiki/Twitter-compatible_API) and the [Twitter API](https://dev.twitter.com/rest/public). + +* [Home](help) + +The Friendica API aims to be compatible to the [GNU Social API](http://wiki.gnusocial.de/gnusocial:api) and the [Twitter API](https://dev.twitter.com/rest/public). Please refer to the linked documentation for further information. ## Implemented API calls ### General +#### HTTP Method + +API endpoints can restrict the method used to request them. +Using an invalid method results in HTTP error 405 "Method Not Allowed". + +In this document, the required method is listed after the endpoint name. "*" means every method can be used. + +#### Auth + +Friendica supports basic http auth and OAuth 1 to authenticate the user to the api. + +OAuth settings can be added by the user in web UI under /settings/oauth/ + +In this document, endpoints which requires auth are marked with "AUTH" after endpoint name + #### Unsupported parameters * cursor: Not implemented in GNU Social * trim_user: Not implemented in GNU Social @@ -24,17 +42,50 @@ Please refer to the linked documentation for further information. * cid: Contact id of the user (important for "contact_allow" and "contact_deny") * network: network of the user -### account/rate_limit_status +#### Errors +When an error occour in API call, an HTTP error code is returned, with an error message +Usually: +- 400 Bad Request: if parameter are missing or items can't be found +- 403 Forbidden: if authenticated user is missing +- 405 Method Not Allowed: if API was called with invalid method, eg. GET when API require POST +- 501 Not Implemented: if requested API doesn't exists +- 500 Internal Server Error: on other error contitions -### account/verify_credentials +Error body is + +json: +``` + { + "error": "Specific error message", + "request": "API path requested", + "code": "HTTP error code" + } +``` + +xml: +``` + + Specific error message + API path requested + HTTP error code + +``` + +--- +### account/rate_limit_status (*; AUTH) + +--- +### account/verify_credentials (*; AUTH) #### Parameters + * skip_status: Don't show the "status" field. (Default: false) * include_entities: "true" shows entities for pictures and links (Default: false) -### conversation/show +--- +### conversation/show (*; AUTH) Unofficial Twitter command. It shows all direct answers (excluding the original post) to a given id. -#### Parameters +#### Parameter * id: id of the post * count: Items per page (default: 20) * page: page number @@ -43,11 +94,12 @@ Unofficial Twitter command. It shows all direct answers (excluding the original * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details +* include_rts +* trim_user +* contributor_details -### direct_messages +--- +### direct_messages (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -55,19 +107,23 @@ Unofficial Twitter command. It shows all direct answers (excluding the original * max_id: maximum id * getText: Defines the format of the status field. Can be "html" or "plain" * include_entities: "true" shows entities for pictures and links (Default: false) +* friendica_verbose: "true" enables different error returns (default: "false") #### Unsupported parameters -* skip_status +* skip_status -### direct_messages/all +--- +### direct_messages/all (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number * since_id: minimal id * max_id: maximum id * getText: Defines the format of the status field. Can be "html" or "plain" +* friendica_verbose: "true" enables different error returns (default: "false") -### direct_messages/conversation +--- +### direct_messages/conversation (*; AUTH) Shows all direct messages of a conversation #### Parameters * count: Items per page (default: 20) @@ -76,16 +132,10 @@ Shows all direct messages of a conversation * max_id: maximum id * getText: Defines the format of the status field. Can be "html" or "plain" * uri: URI of the conversation +* friendica_verbose: "true" enables different error returns (default: "false") -### direct_messages/new -#### Parameters -* user_id: id of the user -* screen_name: screen name (for technical reasons, this value is not unique!) -* text: The message -* replyto: ID of the replied direct message -* title: Title of the direct message - -### direct_messages/sent +--- +### direct_messages/sent (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -93,8 +143,37 @@ Shows all direct messages of a conversation * max_id: maximum id * getText: Defines the format of the status field. Can be "html" or "plain" * include_entities: "true" shows entities for pictures and links (Default: false) +* friendica_verbose: "true" enables different error returns (default: "false") -### favorites +--- +### direct_messages/new (POST,PUT; AUTH) +#### Parameters +* user_id: id of the user +* screen_name: screen name (for technical reasons, this value is not unique!) +* text: The message +* replyto: ID of the replied direct message +* title: Title of the direct message + +--- +### direct_messages/destroy (POST,DELETE; AUTH) +#### Parameters +* id: id of the message to be deleted +* include_entities: optional, currently not yet implemented +* friendica_parenturi: optional, can be used for increased safety to delete only intended messages +* friendica_verbose: "true" enables different error returns (default: "false") + +#### Return values + +On success: +* JSON return as defined for Twitter API not yet implemented +* on friendica_verbose=true: JSON return {"result":"ok","message":"message deleted"} + +On error: +HTTP 400 BadRequest +* on friendica_verbose=true: different JSON returns {"result":"error","message":"xyz"} + +--- +### favorites (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -106,87 +185,96 @@ Shows all direct messages of a conversation * user_id * screen_name -Favorites aren't displayed to other users, so "user_id" and "screen_name". So setting this value will result in an empty array. +Favorites aren't displayed to other users, so "user_id" and "screen_name" are unsupported. +Set this values will result in an empty array. -### favorites/create +--- +### favorites/create (POST,PUT; AUTH) #### Parameters * id * include_entities: "true" shows entities for pictures and links (Default: false) -### favorites/destroy +--- +### favorites/destroy (POST,DELETE; AUTH) #### Parameters * id * include_entities: "true" shows entities for pictures and links (Default: false) -### followers/ids +--- +### followers/ids (*; AUTH) #### Parameters * stringify_ids: Should the id numbers be sent as text (true) or number (false)? (default: false) #### Unsupported parameters * user_id * screen_name -* cursor +* cursor Friendica doesn't allow showing followers of other users. -### friendica/photo -#### Parameters -* photo_id: Resource id of a photo. - -Returns data of a picture with the given resource. - -### friendica/photos/list - -Returns a list of all photo resources of the logged in user. - -### friends/ids +--- +### friends/ids (*; AUTH) #### Parameters * stringify_ids: Should the id numbers be sent as text (true) or number (false)? (default: false) #### Unsupported parameters * user_id * screen_name -* cursor +* cursor Friendica doesn't allow showing friends of other users. -### help/test +--- +### help/test (*) -### media/upload +--- +### media/upload (POST,PUT; AUTH) #### Parameters * media: image data -### oauth/request_token +--- +### oauth/request_token (*) #### Parameters -* oauth_callback +* oauth_callback #### Unsupported parameters -* x_auth_access_type +* x_auth_access_type -### oauth/access_token +--- +### oauth/access_token (*) #### Parameters -* oauth_verifier +* oauth_verifier #### Unsupported parameters -* x_auth_password -* x_auth_username -* x_auth_mode +* x_auth_password +* x_auth_username +* x_auth_mode -### statuses/destroy +--- +### statuses/destroy (POST,DELETE; AUTH) #### Parameters * id: message number * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* trim_user +* trim_user + +--- +### statuses/followers (*; AUTH) + +#### Parameters -### statuses/followers * include_entities: "true" shows entities for pictures and links (Default: false) -### statuses/friends +--- +### statuses/friends (*; AUTH) + +#### Parameters + * include_entities: "true" shows entities for pictures and links (Default: false) -### statuses/friends_timeline +--- +### statuses/friends_timeline (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -197,11 +285,12 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details +* include_rts +* trim_user +* contributor_details -### statuses/home_timeline +--- +### statuses/home_timeline (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -212,11 +301,12 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details +* include_rts +* trim_user +* contributor_details -### statuses/mentions +--- +### statuses/mentions (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -225,11 +315,12 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details +* include_rts +* trim_user +* contributor_details -### statuses/public_timeline +--- +### statuses/public_timeline (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -240,9 +331,10 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* trim_user +* trim_user -### statuses/replies +--- +### statuses/replies (*; AUTH) #### Parameters * count: Items per page (default: 20) * page: page number @@ -251,28 +343,31 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details +* include_rts +* trim_user +* contributor_details -### statuses/retweet +--- +### statuses/retweet (POST,PUT; AUTH) #### Parameters * id: message number * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* trim_user +* trim_user -### statuses/show +--- +### statuses/show (*; AUTH) #### Parameters * id: message number * conversation: if set to "1" show all messages of the conversation with the given id * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_my_retweet -* trim_user +* include_my_retweet +* trim_user +--- ### statuses/update, statuses/update_with_media #### Parameters * title: Title of the status @@ -289,16 +384,17 @@ Friendica doesn't allow showing friends of other users. * contact_deny * network * include_entities: "true" shows entities for pictures and links (Default: false) -* media_ids: (By now only a single value, no array) +* media_ids: (By now only a single value, no array) #### Unsupported parameters * trim_user * place_id * display_coordinates -### statuses/user_timeline +--- +### statuses/user_timeline (*; AUTH) #### Parameters -* user_id: id of the user +* user_id: id of the user * screen_name: screen name (for technical reasons, this value is not unique!) * count: Items per page (default: 20) * page: page number @@ -309,43 +405,381 @@ Friendica doesn't allow showing friends of other users. * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters -* include_rts -* trim_user -* contributor_details -### statusnet/config +* include_rts +* trim_user +* contributor_details -### statusnet/version +--- +### statusnet/config (*) + +--- +### statusnet/conversation (*; AUTH) +It shows all direct answers (excluding the original post) to a given id. + +#### Parameter +* id: id of the post +* count: Items per page (default: 20) +* page: page number +* since_id: minimal id +* max_id: maximum id +* include_entities: "true" shows entities for pictures and links (Default: false) + +--- +### statusnet/version (*) #### Unsupported parameters * user_id * screen_name -* cursor +* cursor Friendica doesn't allow showing followers of other users. -### users/search +--- +### users/search (*) #### Parameters -* q: name of the user +* q: name of the user #### Unsupported parameters * page * count * include_entities -### users/show +--- +### users/show (*) #### Parameters -* user_id: id of the user +* user_id: id of the user * screen_name: screen name (for technical reasons, this value is not unique!) * include_entities: "true" shows entities for pictures and links (Default: false) #### Unsupported parameters * user_id * screen_name -* cursor +* cursor Friendica doesn't allow showing friends of other users. + +## Implemented API calls (not compatible with other APIs) + + +--- +### friendica/activity/ +#### parameters +* id: item id + +Add or remove an activity from an item. +'verb' can be one of: + +- like +- dislike +- attendyes +- attendno +- attendmaybe + +To remove an activity, prepend the verb with "un", eg. "unlike" or "undislike" +Attend verbs disable eachother: that means that if "attendyes" was added to an item, adding "attendno" remove previous "attendyes". +Attend verbs should be used only with event-related items (there is no check at the moment) + +#### Return values + +On success: +json +```"ok"``` + +xml +```true``` + +On error: +HTTP 400 BadRequest + +--- +### friendica/group_show (*; AUTH) +Return all or a specified group of the user with the containing contacts as array. + +#### Parameters +* gid: optional, if not given, API returns all groups of the user + +#### Return values +Array of: + +* name: name of the group +* gid: id of the group +* user: array of group members (return from api_get_user() function for each member) + + +--- +### friendica/group_delete (POST,DELETE; AUTH) +delete the specified group of contacts; API call need to include the correct gid AND name of the group to be deleted. + +#### Parameters +* gid: id of the group to be deleted +* name: name of the group to be deleted + +#### Return values +Array of: + +* success: true if successfully deleted +* gid: gid of the deleted group +* name: name of the deleted group +* status: „deleted“ if successfully deleted +* wrong users: empty array + + +--- +### friendica/group_create (POST,PUT; AUTH) +Create the group with the posted array of contacts as members. + +#### Parameters +* name: name of the group to be created + +#### POST data +JSON data as Array like the result of "users/group_show": + +* gid +* name +* array of users + +#### Return values +Array of: + +* success: true if successfully created or reactivated +* gid: gid of the created group +* name: name of the created group +* status: „missing user“ | „reactivated“ | „ok“ +* wrong users: array of users, which were not available in the contact table + + +--- +### friendica/group_update (POST) +Update the group with the posted array of contacts as members (post all members of the group to the call; function will remove members not posted). + +#### Parameters +* gid: id of the group to be changed +* name: name of the group to be changed + +#### POST data +JSON data as array like the result of „users/group_show“: + +* gid +* name +* array of users + +#### Return values +Array of: + +* success: true if successfully updated +* gid: gid of the changed group +* name: name of the changed group +* status: „missing user“ | „ok“ +* wrong users: array of users, which were not available in the contact table + + + +--- +### friendica/notifications (GET) +Return last 50 notification for current user, ordered by date with unseen item on top + +#### Parameters +none + +#### Return values +Array of: + +* id: id of the note +* type: type of notification as int (see NOTIFY_* constants in boot.php) +* name: full name of the contact subject of the note +* url: contact's profile url +* photo: contact's profile photo +* date: datetime string of the note +* timestamp: timestamp of the node +* date_rel: relative date of the note (eg. "1 hour ago") +* msg: note message in bbcode +* msg_html: note message in html +* msg_plain: note message in plain text +* link: link to note +* seen: seen state: 0 or 1 + + +--- +### friendica/notifications/seen (POST) +Set note as seen, returns item object if possible + +#### Parameters +id: id of the note to set seen + +#### Return values +If the note is linked to an item, the item is returned, just like one of the "statuses/*_timeline" api. + +If the note is not linked to an item, a success status is returned: + +* "success" (json) | "<status>success</status>" (xml) + + +--- +### friendica/photo (*; AUTH) +#### Parameters +* photo_id: Resource id of a photo. +* scale: (optional) scale value of the photo + +Returns data of a picture with the given resource. +If 'scale' isn't provided, returned data include full url to each scale of the photo. +If 'scale' is set, returned data include image data base64 encoded. + +possibile scale value are: + +* 0: original or max size by server settings +* 1: image with or height at <= 640 +* 2: image with or height at <= 320 +* 3: thumbnail 160x160 +* 4: Profile image at 175x175 +* 5: Profile image at 80x80 +* 6: Profile image at 48x48 + +An image used as profile image has only scale 4-6, other images only 0-3 + +#### Return values + +json +``` + { + "id": "photo id" + "created": "date(YYYY-MM-GG HH:MM:SS)", + "edited": "date(YYYY-MM-GG HH:MM:SS)", + "title": "photo title", + "desc": "photo description", + "album": "album name", + "filename": "original file name", + "type": "mime type", + "height": "number", + "width": "number", + "profile": "1 if is profile photo", + "link": { + "": "url to image" + ... + }, + // if 'scale' is set + "datasize": "size in byte", + "data": "base64 encoded image data" + } +``` + +xml +``` + + photo id + date(YYYY-MM-GG HH:MM:SS) + date(YYYY-MM-GG HH:MM:SS) + photo title + photo description + album name + original file name + mime type + number + number + 1 if is profile photo + + + ... + + +``` + +--- +### friendica/photos/list (*; AUTH) + +Returns a list of all photo resources of the logged in user. + +#### Return values + +json +``` + [ + { + id: "resource_id", + album: "album name", + filename: "original file name", + type: "image mime type", + thumb: "url to thumb sized image" + }, + ... + ] +``` + +xml +``` + + + "url to thumb sized image" + + ... + +``` + +--- +### friendica/direct_messages_setseen (GET; AUTH) +#### Parameters +* id: id of the message to be updated as seen + +#### Return values + +On success: +* JSON return {"result":"ok","message":"message set to seen"} + +On error: +* different JSON returns {"result":"error","message":"xyz"} + +--- +### friendica/direct_messages_search (GET; AUTH) +#### Parameters +* searchstring: string for which the API call should search as '%searchstring%' in field 'body' of all messages of the authenticated user (caption ignored) + +#### Return values +Returns only tested with JSON, XML might work as well. + +On success: +* JSON return {"success":"true","search_results": array of found messages} +* JSOn return {"success":"false","search_results":"nothing found"} + +On error: +* different JSON returns {"result":"error","message":"searchstring not specified"} + +--- +### friendica/profile/show (GET; AUTH) +show data of all profiles or a single profile of the authenticated user + +#### Parameters +* profile_id: id of the profile to be returned (optional, if omitted all profiles are returned by default) + +#### Return values +On success: Array of: + +* multi_profiles: true if user has activated multi_profiles +* global_dir: URL of the global directory set in server settings +* friendica_owner: user data of the authenticated user +* profiles: array of the profile data + +On error: +HTTP 403 Forbidden: when no authentication provided +HTTP 400 Bad Request: if given profile_id is not in db or not assigned to authenticated user + +General description of profile data in API returns: +* profile_id +* profile_name +* is_default: true if this is the public profile +* hide_friends: true if friends are hidden +* profile_photo +* profile_thumb +* publish: true if published on the server's local directory +* net_publish: true if published to global_dir +* description ... homepage: different data fields from 'profile' table in database +* users: array with the users allowed to view this profile (empty if is_default=true) + + +--- ## Not Implemented API calls The following API calls are implemented in GNU Social but not in Friendica: (incomplete) @@ -368,7 +802,6 @@ The following API calls from the Twitter API aren't implemented neither in Frien * statuses/lookup * direct_messages/show * search/tweets -* direct_messages/destroy * friendships/no_retweets/ids * friendships/incoming * friendships/outgoing @@ -432,17 +865,21 @@ The following API calls from the Twitter API aren't implemented neither in Frien * trends/closest * users/report_spam +--- + +--- + ## Usage Examples ### BASH / cURL Betamax has documentated some example API usage from a [bash script](https://en.wikipedia.org/wiki/Bash_(Unix_shell) employing [curl](https://en.wikipedia.org/wiki/CURL) (see [his posting](https://betamax65.de/display/betamax65/43539)). - /usr/bin/curl -u USER:PASS https://YOUR.FRIENDICA.TLD/api/statuses/update.xml -d source="some source id" -d status="the status you want to post" +/usr/bin/curl -u USER:PASS https://YOUR.FRIENDICA.TLD/api/statuses/update.xml -d source="some source id" -d status="the status you want to post" ### Python The [RSStoFriedika](https://github.com/pafcu/RSStoFriendika) code can be used as an example of how to use the API with python. The lines for posting are located at [line 21](https://github.com/pafcu/RSStoFriendika/blob/master/RSStoFriendika.py#L21) and following. - def tweet(server, message, group_allow=None): - url = server + '/api/statuses/update' - urllib2.urlopen(url, urllib.urlencode({'status': message,'group_allow[]':group_allow}, doseq=True)) +def tweet(server, message, group_allow=None): +url = server + '/api/statuses/update' +urllib2.urlopen(url, urllib.urlencode({'status': message,'group_allow[]':group_allow}, doseq=True)) There is also a [module for python 3](https://bitbucket.org/tobiasd/python-friendica) for using the API. diff --git a/doc/autoloader.md b/doc/autoloader.md new file mode 100644 index 0000000000..69c62451cd --- /dev/null +++ b/doc/autoloader.md @@ -0,0 +1,209 @@ +Autoloader +========== + +* [Home](help) + +There is some initial support to class autoloading in Friendica core. + +The autoloader code is in `include/autoloader.php`. +It's derived from composer autoloader code. + +Namespaces and Classes are mapped to folders and files in `library/`, +and the map must be updated by hand, because we don't use composer yet. +The mapping is defined by files in `include/autoloader/` folder. + +Currently, only HTMLPurifier library is loaded using autoloader. + + +## A quick introdution to class autoloading + +The autoloader it's a way for php to automagically include the file that define a class when the class is first used, without the need to use "require_once" every time. + +Once is setup you don't have to use it in any way. You need a class? you use the class. + +At his basic is a function passed to the "spl_autoload_register()" function, which receive as argument the class name the script want and is it job to include the correct php file where that class is defined. +The best source for documentation is [php site](http://php.net/manual/en/language.oop5.autoload.php). + +One example, based on fictional friendica code. + +Let's say you have a php file in "include/" that define a very useful class: + +``` + file: include/ItemsManager.php + array($baseDir."/include"); + ); +``` + + +That tells the autoloader code to look for files that defines classes in "Friendica" namespace under "include/" folder. (And btw, that's why the file has the same name as the class it defines.) + +*note*: The structure of files in "include/autoloader/" has been copied from the code generated by composer, to ease the work of enable autoloader for external libraries under "library/" + +Let's say now that you need to load some items in a view, maybe in a fictional "mod/network.php". +Somewere at the start of the scripts, the autoloader was initialized. In Friendica is done at the top of "boot.php", with "require_once('include/autoloader.php');". + +The code will be something like: + +``` + file: mod/network.php + getAll(); + + // pass $items to template + // return result + } +``` + +That's a quite simple example, but look: no "require()"! +You need to use a class, you use the class and you don't need to do anything more. + +Going further: now we have a bunch of "*Manager" classes that cause some code duplication, let's define a BaseManager class, where to move all code in common between all managers: + +``` + file: include/BaseManager.php + <78> | mediumtext | NO | | NULL | | +| allow_gid | Access Control - list of allowed groups | mediumtext | NO | | NULL | | +| deny_cid | Access Control - list of denied contact.id | mediumtext | NO | | NULL | | +| deny_gid | Access Control - list of denied groups | mediumtext | NO | | NULL | | + +Notes: Permissions are surrounded by angle chars. e.g. <4> + +Return to [database documentation](help/database) diff --git a/doc/database/db_auth_codes.md b/doc/database/db_auth_codes.md new file mode 100644 index 0000000000..1fec38500d --- /dev/null +++ b/doc/database/db_auth_codes.md @@ -0,0 +1,14 @@ +Table auth_codes +================ + +OAuth2 authorisation register - currently implemented but unused + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------- | ----------- | ------------ | ---- | --- | ------- | ----- | +| id | | varchar(40) | NO | PRI | NULL | | +| client_id | | varchar(20) | NO | | | | +| redirect_uri | | varchar(200) | NO | | | | +| expires | | int(11) | NO | | 0 | | +| scope | | varchar(250) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_cache.md b/doc/database/db_cache.md new file mode 100644 index 0000000000..333adaa2c1 --- /dev/null +++ b/doc/database/db_cache.md @@ -0,0 +1,11 @@ +Table cache +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------ | ---------------------------------- | ------------ | ---- | --- | ------------------- | ----- | +| k | horizontal width + url or resource | varchar(255) | NO | PRI | NULL | | +| v | OEmbed response from site | text | NO | | NULL | | +| updated | datetime of cache insertion | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| expire_mode | | int(11) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_challenge.md b/doc/database/db_challenge.md new file mode 100644 index 0000000000..aa7b263fdf --- /dev/null +++ b/doc/database/db_challenge.md @@ -0,0 +1,13 @@ +Table challenge +=============== + +| Field | Description | Type | Null | Key | Default | Extra | +|-------------|------------------|------------------|------|-----|---------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| challenge | | varchar(255) | NO | | | | +| dfrn-id | | varchar(255) | NO | | | | +| expire | | int(11) | NO | | 0 | | +| type | | varchar(255) | NO | | | | +| last_update | | varchar(255) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_clients.md b/doc/database/db_clients.md new file mode 100644 index 0000000000..228b45cc21 --- /dev/null +++ b/doc/database/db_clients.md @@ -0,0 +1,13 @@ +Table clients +============= + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------- | ----------- | ------------ | ---- | --- | ------- | ----- | +| client_id | | varchar(20) | NO | PRI | NULL | | +| pw | | varchar(20) | NO | | | | +| redirect_uri | | varchar(200) | NO | | | | +| name | | text | YES | | NULL | | +| icon | | text | YES | | NULL | | +| uid | | int(11) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_config.md b/doc/database/db_config.md new file mode 100644 index 0000000000..6bcb4bcf89 --- /dev/null +++ b/doc/database/db_config.md @@ -0,0 +1,11 @@ +Table config +============ + +| Field | Description | Type | Null | Key | Default | Extra | +| ----- | ----------- | ---------------- | ---- | --- | ------- | --------------- | +| id | | int(10) unsigned | NO | PRI | NULL | auto_increment | +| cat | | char(255) | NO | MUL | | | +| k | | char(255) | NO | | | | +| v | | text | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_contact.md b/doc/database/db_contact.md new file mode 100644 index 0000000000..2e3ebc3130 --- /dev/null +++ b/doc/database/db_contact.md @@ -0,0 +1,72 @@ +Table contact +============= + +| Field | Description | Type | Null | Key | Default | Extra | +|---------------------------|-----------------------------------------------------------|--------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | user.id of the owner of this data | int(11) | NO | MUL | 0 | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | +| self | 1 if the contact is the user him/her self | tinyint(1) | NO | | 0 | | +| remote_self | | tinyint(1) | NO | | 0 | | +| rel | The kind of the relation between the user and the contact | tinyint(1) | NO | | 0 | | +| duplex | | tinyint(1) | NO | | 0 | | +| network | Network protocol of the contact | varchar(255) | NO | | | | +| name | Name that this contact is known by | varchar(255) | NO | | | | +| nick | Nick- and user name of the contact | varchar(255) | NO | | | | +| location | | varchar(255) | NO | | | | +| about | | text | NO | | NULL | | +| keywords | public keywords (interests) of the contact | text | NO | | NULL | | +| gender | | varchar(32) | NO | | | | +| attag | | varchar(255) | NO | | | | +| photo | Link to the profile photo of the contact | text | NO | | NULL | | +| thumb | Link to the profile photo (thumb size) | text | NO | | NULL | | +| micro | Link to the profile photo (micro size) | text | NO | | NULL | | +| site-pubkey | | text | NO | | NULL | | +| issued-id | | varchar(255) | NO | | | | +| dfrn-id | | varchar(255) | NO | | | | +| url | | varchar(255) | NO | | | | +| nurl | | varchar(255) | NO | | | | +| addr | | varchar(255) | NO | | | | +| alias | | varchar(255) | NO | | | | +| pubkey | RSA public key 4096 bit | text | NO | | NULL | | +| prvkey | RSA private key 4096 bit | text | NO | | NULL | | +| batch | | varchar(255) | NO | | | | +| request | | text | NO | | NULL | | +| notify | | text | NO | | NULL | | +| poll | | text | NO | | NULL | | +| confirm | | text | NO | | NULL | | +| poco | | text | NO | | NULL | | +| aes_allow | | tinyint(1) | NO | | 0 | | +| ret-aes | | tinyint(1) | NO | | 0 | | +| usehub | | tinyint(1) | NO | | 0 | | +| subhub | | tinyint(1) | NO | | 0 | | +| hub-verify | | varchar(255) | NO | | | | +| last-update | Date of the last try to update the contact info | datetime | NO | | 0000-00-00 00:00:00 | | +| success_update | Date of the last successful contact update | datetime | NO | | 0000-00-00 00:00:00 | | +| failure_update | Date of the last failed update | datetime | NO | | 0000-00-00 00:00:00 | | +| name-date | | datetime | NO | | 0000-00-00 00:00:00 | | +| uri-date | | datetime | NO | | 0000-00-00 00:00:00 | | +| avatar-date | | datetime | NO | | 0000-00-00 00:00:00 | | +| term-date | | datetime | NO | | 0000-00-00 00:00:00 | | +| last-item | date of the last post | datetime | NO | | 0000-00-00 00:00:00 | | +| priority | | tinyint(3) | NO | | 0 | | +| blocked | | tinyint(1) | NO | | 1 | | +| readonly | posts of the contact are readonly | tinyint(1) | NO | | 0 | | +| writable | | tinyint(1) | NO | | 0 | | +| forum | contact is a forum | tinyint(1) | NO | | 0 | | +| prv | contact is a private group | tinyint(1) | NO | | 0 | | +| hidden | | tinyint(1) | NO | | 0 | | +| archive | | tinyint(1) | NO | | 0 | | +| pending | | tinyint(1) | NO | | 1 | | +| rating | | tinyint(1) | NO | | 0 | | +| reason | | text | NO | | NULL | | +| closeness | | tinyint(2) | NO | | 99 | | +| info | | mediumtext | NO | | NULL | | +| profile-id | | int(11) | NO | | 0 | | +| bdyear | | varchar(4) | NO | | | | +| bd | | date | NO | | 0000-00-00 | | +| notify_new_posts | | tinyint(1) | NO | | 0 | | +| fetch_further_information | | tinyint(1) | NO | | 0 | | +| ffi_keyword_blacklist | | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_conv.md b/doc/database/db_conv.md new file mode 100644 index 0000000000..f20074d61d --- /dev/null +++ b/doc/database/db_conv.md @@ -0,0 +1,15 @@ +Table conv +========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | ----------------------------------------- | ---------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| guid | A unique identifier for this conversation | varchar(64) | NO | | | | +| recips | sender_handle;recipient_handle | mediumtext | NO | | NULL | | +| uid | user_id of the owner of this data | int(11) | NO | MUL | 0 | | +| creator | handle of creator | varchar(255) | NO | | | | +| created | creation timestamp | datetime | NO | | 0000-00-00 00:00:00 | | +| updated | edited timestamp | datetime | NO | | 0000-00-00 00:00:00 | | +| subject | subject of initial message | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_deliverq.md b/doc/database/db_deliverq.md new file mode 100644 index 0000000000..5335899571 --- /dev/null +++ b/doc/database/db_deliverq.md @@ -0,0 +1,12 @@ +Table deliverq +============== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|------------------|------|-----|---------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| cmd | | varchar(32) | NO | | | | +| item | | int(11) | NO | | 0 | | +| contact | | int(11) | NO | | 0 | | + + +Return to [database documentation](help/database) diff --git a/doc/database/db_event.md b/doc/database/db_event.md new file mode 100644 index 0000000000..d45ddd2063 --- /dev/null +++ b/doc/database/db_event.md @@ -0,0 +1,26 @@ +Table event +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ---------- | ----------------------------------------------- -------| ------------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | user_id of the owner of this data | int(11) | NO | MUL | 0 | | +| cid | contact_id (ID of the contact in contact table) | int(11) | NO | | 0 | | +| uri | | varchar(255) | NO | | | | +| created | creation time | datetime | NO | | 0000-00-00 00:00:00 | | +| edited | last edit time | datetime | NO | | 0000-00-00 00:00:00 | | +| start | event start time | datetime | NO | | 0000-00-00 00:00:00 | | +| finish | event end time | datetime | NO | | 0000-00-00 00:00:00 | | +| summary | short description or title of the event | text | NO | | NULL | | +| desc | event description | text | NO | | NULL | | +| location | event location | text | NO | | NULL | | +| type | event or birthday | varchar(255) | NO | | | | +| nofinish | if event does have no end this is 1 | tinyint(1) | NO | | 0 | | +| adjust | adjust to timezone of the recipient (0 or 1) | tinyint(1) | NO | | 1 | | +| ignore | 0 or 1 | tinyint(1) unsigned | NO | | 0 | | +| allow_cid | Access Control - list of allowed contact.id '<19><78>' | mediumtext | NO | | NULL | | +| allow_gid | Access Control - list of allowed groups | mediumtext | NO | | NULL | | +| deny_cid | Access Control - list of denied contact.id | mediumtext | NO | | NULL | | +| deny_gid | Access Control - list of denied groups | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_fcontact.md b/doc/database/db_fcontact.md new file mode 100644 index 0000000000..2801605229 --- /dev/null +++ b/doc/database/db_fcontact.md @@ -0,0 +1,24 @@ +Table fcontact +============== + +| Field | Description | Type | Null | Key | Default | Extra | +| -------- | ------------- | ---------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| guid | unique id | varchar(64) | NO | | | | +| url | | varchar(255) | NO | | | | +| name | | varchar(255) | NO | | | | +| photo | | varchar(255) | NO | | | | +| request | | varchar(255) | NO | | | | +| nick | | varchar(255) | NO | | | | +| addr | | varchar(255) | NO | MUL | | | +| batch | | varchar(255) | NO | | | | +| notify | | varchar(255) | NO | | | | +| poll | | varchar(255) | NO | | | | +| confirm | | varchar(255) | NO | | | | +| priority | | tinyint(1) | NO | | 0 | | +| network | | varchar(32) | NO | | | | +| alias | | varchar(255) | NO | | | | +| pubkey | | text | NO | | NULL | | +| updated | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_ffinder.md b/doc/database/db_ffinder.md new file mode 100644 index 0000000000..29fea20d52 --- /dev/null +++ b/doc/database/db_ffinder.md @@ -0,0 +1,11 @@ +Table ffinder +============= + +| Field | Description | Type | Null | Key | Default | Extra | +| ----- | ----------- | ---------------- | ---- | --- | ------- | --------------- | +| id | | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | | int(10) unsigned | NO | | 0 | | +| cid | | int(10) unsigned | NO | | 0 | | +| fid | | int(10) unsigned | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_fserver.md b/doc/database/db_fserver.md new file mode 100644 index 0000000000..5350676274 --- /dev/null +++ b/doc/database/db_fserver.md @@ -0,0 +1,11 @@ +Table fserver +============= + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | ----------- | ------------ | ---- | --- | ------- | --------------- | +| id | | int(11) | NO | PRI | NULL | auto_increment | +| server | | varchar(255) | NO | MUL | | | +| posturl | | varchar(255) | NO | | | | +| key | | text | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_fsuggest.md b/doc/database/db_fsuggest.md new file mode 100644 index 0000000000..b8751a478c --- /dev/null +++ b/doc/database/db_fsuggest.md @@ -0,0 +1,16 @@ +Table fsuggest +============== + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | ----------- | ------------ | ---- | --- | ------------------- | --------------- | +| id | | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | | 0 | | +| cid | | int(11) | NO | | 0 | | +| name | | varchar(255) | NO | | | | +| url | | varchar(255) | NO | | | | +| request | | varchar(255) | NO | | | | +| photo | | varchar(255) | NO | | | | +| note | | text | NO | | NULL | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_gcign.md b/doc/database/db_gcign.md new file mode 100644 index 0000000000..9f5bbce76c --- /dev/null +++ b/doc/database/db_gcign.md @@ -0,0 +1,10 @@ +Table gcign +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ----- | ------------------------------ | ------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | local user.id | int(11) | NO | MUL | 0 | | +| gcid | gcontact.id of ignored contact | int(11) | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_gcontact.md b/doc/database/db_gcontact.md new file mode 100644 index 0000000000..c47e726228 --- /dev/null +++ b/doc/database/db_gcontact.md @@ -0,0 +1,32 @@ +Table gcontact +============== + +| Field |Description | Type | Null | Key | Default | Extra | +|--------------|------------------------------------|------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| name | Name that this contact is known by | varchar(255) | NO | | | | +| nick | Nick- and user name of the contact | varchar(255) | NO | | | | +| url | Link to the contacts profile page | varchar(255) | NO | | | | +| nurl | | varchar(255) | NO | MUL | | | +| photo | Link to the profile photo | varchar(255) | NO | | | | +| connect | | varchar(255) | NO | | | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | +| updated | | datetime | YES | MUL | 0000-00-00 00:00:00 | | +| last_contact | | datetime | YES | | 0000-00-00 00:00:00 | | +| last_failure | | datetime | YES | | 0000-00-00 00:00:00 | | +| location | | varchar(255) | NO | | | | +| about | | text | NO | | NULL | | +| keywords | puplic keywords (interests) | text | NO | | NULL | | +| gender | | varchar(32) | NO | | | | +| birthday | | varchar(32) | NO | | 0000-00-00 | | +| community | 1 if contact is forum account | tinyint(1) | NO | | 0 | | +| hide | 1 = should be hidden from search | tinyint(1) | NO | | 0 | | +| nsfw | 1 = contact posts nsfw content | tinyint(1) | NO | | 0 | | +| network | social network protocol | varchar(255) | NO | | | | +| addr | | varchar(255) | NO | | | | +| notify | | text | NO | | | | +| alias | | varchar(255) | NO | | | | +| generation | | tinyint(3) | NO | | 0 | | +| server_url | baseurl of the contacts server | varchar(255) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_glink.md b/doc/database/db_glink.md new file mode 100644 index 0000000000..eb2f10bc67 --- /dev/null +++ b/doc/database/db_glink.md @@ -0,0 +1,13 @@ +Table glink +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| cid | | int(11) | NO | MUL | 0 | | +| uid | | int(11) | NO | | 0 | | +| gcid | | int(11) | NO | MUL | 0 | | +| zcid | | int(11) | NO | MUL | 0 | | +| updated | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_group.md b/doc/database/db_group.md new file mode 100644 index 0000000000..f27b9a75f7 --- /dev/null +++ b/doc/database/db_group.md @@ -0,0 +1,12 @@ +Table group +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | ------------------------------------------ | ---------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | user.id owning this data | int(10) unsigned | NO | MUL | 0 | | +| visible | 1 indicates the member list is not private | tinyint(1) | NO | | 0 | | +| deleted | 1 indicates the group has been deleted | tinyint(1) | NO | | 0 | | +| name | human readable name of group | varchar(255) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_group_member.md b/doc/database/db_group_member.md new file mode 100644 index 0000000000..f8a402ef6e --- /dev/null +++ b/doc/database/db_group_member.md @@ -0,0 +1,11 @@ +Table group_member +================== + +| Field | Description | Type | Null | Key | Default | Extra | +| ---------- | ----------------------------------------------------------- | ---------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | user.id of the owner of this data | int(10) unsigned | NO | MUL | 0 | | +| gid | groups.id of the associated group | int(10) unsigned | NO | | 0 | | +| contact-id | contact.id of the member assigned to the associated group | int(10) unsigned | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_gserver.md b/doc/database/db_gserver.md new file mode 100644 index 0000000000..b62b802726 --- /dev/null +++ b/doc/database/db_gserver.md @@ -0,0 +1,23 @@ +Table gserver +============= + +| Field | Description | Type | Null | Key | Default | Extra | +|-----------------|------------------|------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| url | | varchar(255) | NO | | | | +| nurl | | varchar(255) | NO | MUL | | | +| version | | varchar(255) | NO | | | | +| site_name | | varchar(255) | NO | | | | +| info | | text | NO | | NULL | | +| register_policy | | tinyint(1) | NO | | 0 | | +| poco | | varchar(255) | NO | | | | +| noscrape | | varchar(255) | NO | | | | +| network | | varchar(32) | NO | | | | +| platform | | varchar(255) | NO | | | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | +| last_poco_query | | datetime | YES | | 0000-00-00 00:00:00 | | +| last_contact | | datetime | YES | | 0000-00-00 00:00:00 | | +| last_failure | | datetime | YES | | 0000-00-00 00:00:00 | | + + +Return to [database documentation](help/database) diff --git a/doc/database/db_hook.md b/doc/database/db_hook.md new file mode 100644 index 0000000000..06eb84a182 --- /dev/null +++ b/doc/database/db_hook.md @@ -0,0 +1,12 @@ +Table hook +========== + +| Field | Description | Type | Null | Key | Default | Extra | +| -------- | ---------------------------------------------------------------------------------------------------------- | ---------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| hook | name of hook | varchar(255) | NO | MUL | | | +| file | relative filename of hook handler | varchar(255) | NO | | | | +| function | function name of hook handler | varchar(255) | NO | | | | +| priority | not yet implemented - can be used to sort conflicts in hook handling by calling handlers in priority order | int(11) unsigned | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_intro.md b/doc/database/db_intro.md new file mode 100644 index 0000000000..9eb68532c4 --- /dev/null +++ b/doc/database/db_intro.md @@ -0,0 +1,18 @@ +Table intro +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +|------------|------------------|------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | | int(10) unsigned | NO | | 0 | | +| fid | | int(11) | NO | | 0 | | +| contact-id | | int(11) | NO | | 0 | | +| knowyou | | tinyint(1) | NO | | 0 | | +| duplex | | tinyint(1) | NO | | 0 | | +| note | | text | NO | | NULL | | +| hash | | varchar(255) | NO | | | | +| datetime | | datetime | NO | | 0000-00-00 00:00:00 | | +| blocked | | tinyint(1) | NO | | 1 | | +| ignore | | tinyint(1) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_item.md b/doc/database/db_item.md new file mode 100644 index 0000000000..7981d29956 --- /dev/null +++ b/doc/database/db_item.md @@ -0,0 +1,72 @@ +Table item +========== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| guid | A unique identifier for this item | varchar(255) | NO | MUL | | | +| uri | | varchar(255) | NO | MUL | | | +| uid | user.id which owns this copy of the item | int(10) unsigned | NO | MUL | 0 | | +| contact-id | contact.id | int(11) | NO | MUL | 0 | | +| gcontact-id | ID of the global contact | int(11) | NO | MUL | 0 | | +| type | | varchar(255) | NO | | | | +| wall | This item was posted to the wall of uid | tinyint(1) | NO | MUL | 0 | | +| gravity | | tinyint(1) | NO | | 0 | | +| parent | item.id of the parent to this item if it is a reply of some form; otherwise this must be set to the id of this item | int(10) unsigned | NO | MUL | 0 | | +| parent-uri | uri of the parent to this item | varchar(255) | NO | MUL | | | +| extid | | varchar(255 | NO | MUL | | | +| thr-parent | If the parent of this item is not the top-level item in the conversation, the uri of the immediate parent; otherwise set to parent-uri | varchar(255) | NO | | | | +| created | Creation timestamp. | datetime | NO | | 0000-00-00 00:00:00 | | +| edited | Date of last edit (default is created) | datetime | NO | | 0000-00-00 00:00:00 | | +| commented | Date of last comment/reply to this item | datetime | NO | | 0000-00-00 00:00:00 | | +| received | datetime | datetime | NO | | 0000-00-00 00:00:00 | | +| changed | Date that something in the conversation changed, indicating clients should fetch the conversation again | datetime | NO | | 0000-00-00 00:00:00 | | +| owner-name | Name of the owner of this item | varchar(255) | NO | | | | +| owner-link | Link to the profile page of the owner of this item | varchar(255) | NO | | | | +| owner-avatar | Link to the avatar picture of the owner of this item | varchar(255) | NO | | | | +| owner-id | Link to the contact table with uid=0 of the owner of this item | int(11) | NO | MUL | 0 | | +| author-name | Name of the author of this item | varchar(255) | NO | | | | +| author-link | Link to the profile page of the author of this item | varchar(255) | NO | | | | +| author-avatar | Link to the avatar picture of the author of this item | varchar(255) | NO | | | | +| author-id | Link to the contact table with uid=0 of the author of this item | int(11) | NO | MUL | 0 | | +| title | item title | varchar(255) | NO | | | | +| body | item body content | mediumtext | NO | | NULL | | +| app | application which generated this item | varchar(255) | NO | | | | +| verb | ActivityStreams verb | varchar(255) | NO | | | | +| object-type | ActivityStreams object type | varchar(255) | NO | | | | +| object | JSON encoded object structure unless it is an implied object (normal post) | text | NO | | NULL | | +| target-type | ActivityStreams target type if applicable (URI) | varchar(255) | NO | | | | +| target | JSON encoded target structure if used | text | NO | | NULL | | +| postopts | External post connectors add their network name to this comma-separated string to identify that they should be delivered to these networks during delivery | text | NO | | NULL | | +| plink | permalink or URL toa displayable copy of the message at its source | varchar(255) | NO | | | | +| resource-id | Used to link other tables to items, it identifies the linked resource (e.g. photo) and if set must also set resource_type | varchar(255) | NO | MUL | | | +| event-id | Used to link to the event.id | int(11) | NO | | 0 | | +| tag | | mediumtext | NO | | NULL | | +| attach | JSON structure representing attachments to this item | mediumtext | NO | | NULL | | +| inform | | mediumtext | NO | | NULL | | +| file | | mediumtext | NO | | NULL | | +| location | text location where this item originated | varchar(255) | NO | | | | +| coord | longitude/latitude pair representing location where this item originated | varchar(255) | NO | | | | +| allow_cid | Access Control - list of allowed contact.id '<19><78>' | mediumtext | NO | | NULL | | +| allow_gid | Access Control - list of allowed groups | mediumtext | NO | | NULL | | +| deny_cid | Access Control - list of denied contact.id | mediumtext | NO | | NULL | | +| deny_gid | Access Control - list of denied groups | mediumtext | NO | | NULL | | +| private | distribution is restricted | tinyint(1) | NO | | 0 | | +| pubmail | | tinyint(1) | NO | | 0 | | +| moderated | | tinyint(1) | NO | | 0 | | +| visible | | tinyint(1) | NO | | 0 | | +| spam | | tinyint(1) | NO | | 0 | | +| starred | item has been favourited | tinyint(1) | NO | | 0 | | +| bookmark | item has been bookmarked | tinyint(1) | NO | | 0 | | +| unseen | item has not been seen | tinyint(1) | NO | | 1 | | +| deleted | item has been deleted | tinyint(1) | NO | MUL | 0 | | +| origin | item originated at this site | tinyint(1) | NO | | 0 | | +| forum_mode | | tinyint(1) | NO | | 0 | | +| last-child | | tinyint(1) unsigned | NO | | 1 | | +| mention | The owner of this item was mentioned in it | tinyint(1) | NO | | 0 | | +| network | Network from where the item comes from | varchar(32) | NO | | | | +| rendered-hash | | varchar(32) | NO | | | | +| rendered-html | item.body converted to html | mediumtext | NO | | NULL | | +| global | | tinyint(1) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_item_id.md b/doc/database/db_item_id.md new file mode 100644 index 0000000000..c578617000 --- /dev/null +++ b/doc/database/db_item_id.md @@ -0,0 +1,12 @@ +Table item_id +============= + +| Field | Description | Type | Null | Key | Default | Extra | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | ---- | --- | ------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| iid | item.id of the referenced item | int(11) | NO | MUL | 0 | | +| uid | user.id of the owner of this data | int(11) | NO | MUL | 0 | | +| sid | an additional identifier to attach or link to the referenced item (often used to store a message_id from another system in order to suppress duplicates) | varchar(255) | NO | MUL | | | +| service | the name or description of the service which generated this identifier | varchar(255) | NO | MUL | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_locks.md b/doc/database/db_locks.md new file mode 100644 index 0000000000..f9b93ff666 --- /dev/null +++ b/doc/database/db_locks.md @@ -0,0 +1,11 @@ +Table locks +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|--------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| name | | varchar(128) | NO | | | | +| locked | | tinyint(1) | NO | | 0 | | +| created | | datetime | YES | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_mail.md b/doc/database/db_mail.md new file mode 100644 index 0000000000..7047da96a5 --- /dev/null +++ b/doc/database/db_mail.md @@ -0,0 +1,24 @@ +Table mail +========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ---------- | -------------------------------------------- | ---------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | user.id of the owner of this data | int(10) unsigned | NO | MUL | 0 | | +| guid | A unique identifier for this private message | int(10) unsigned | NO | MUL | | | +| from-name | name of the sender | varchar(255) | NO | | | | +| from-photo | contact photo link of the sender | varchar(255) | NO | | | | +| from-url | profile linke of the sender | varchar(255) | NO | | | | +| contact-id | contact.id | varchar(255) | NO | | | | +| convid | conv.id | int(11) unsigned | NO | MUL | 0 | | +| title | | varchar(255) | NO | | | | +| body | | mediumtext | NO | | NULL | | +| seen | if message visited it is 1 | varchar(255) | NO | | 0 | | +| reply | | varchar(255) | NO | MUL | 0 | | +| replied | | varchar(255) | NO | | 0 | | +| unknown | if sender not in the contact table this is 1 | varchar(255) | NO | | 0 | | +| uri | | varchar(255) | NO | MUL | | | +| parent-uri | | varchar(255) | NO | MUL | | | +| created | creation time of the private message | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_mailacct.md b/doc/database/db_mailacct.md new file mode 100644 index 0000000000..1c2deb8153 --- /dev/null +++ b/doc/database/db_mailacct.md @@ -0,0 +1,20 @@ +Table mailacct +============== + +| Field | Description | Type | Null | Key | Default | Extra | +|--------------|------------------|--------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | | 0 | | +| server | | varchar(255) | NO | | | | +| port | | int(11) | NO | | 0 | | +| ssltype | | varchar(16) | NO | | | | +| mailbox | | varchar(255) | NO | | | | +| user | | varchar(255) | NO | | | | +| pass | | text | NO | | NULL | | +| reply_to | | varchar(255) | NO | | | | +| action | | int(11) | NO | | 0 | | +| movetofolder | | varchar(255) | NO | | | | +| pubmail | | tinyint(1) | NO | | 0 | | +| last_check | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_manage.md b/doc/database/db_manage.md new file mode 100644 index 0000000000..4cfc1d0a11 --- /dev/null +++ b/doc/database/db_manage.md @@ -0,0 +1,10 @@ +Table manage +============ + +| Field | Description | Type | Null | Key | Default | Extra | +| ----- | ------------- | ------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | user.id | int(11) | NO | MUL | 0 | | +| mid | | int(11) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_notify-threads.md b/doc/database/db_notify-threads.md new file mode 100644 index 0000000000..5c196628d4 --- /dev/null +++ b/doc/database/db_notify-threads.md @@ -0,0 +1,12 @@ +Table notify-threads +==================== + +| Field | Description | Type | Null | Key | Default | Extra | +|--------------------|------------------|------------------|------|-----|---------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| notify-id | | int(11) | NO | | 0 | | +| master-parent-item | | int(10) unsigned | NO | MUL | 0 | | +| parent-item | | int(10) unsigned | NO | | 0 | | +| receiver-uid | | int(11) | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_notify.md b/doc/database/db_notify.md new file mode 100644 index 0000000000..b2bae64717 --- /dev/null +++ b/doc/database/db_notify.md @@ -0,0 +1,24 @@ +Table notify +============ + +| Field | Description | Type | Null | Key | Default | Extra | +| ---------- | --------------------------------- | ------------ | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| hash | | varchar(64) | NO | | | | +| type | | int(11) | NO | | 0 | | +| name | | varchar(255) | NO | | | | +| url | | varchar(255) | NO | | | | +| photo | | varchar(255) | NO | | | | +| date | | datetime | NO | | 0000-00-00 00:00:00 | | +| msg | | mediumtext | YES | | NULL | | +| uid | user.id of the owner of this data | int(11) | NO | MUL | 0 | | +| link | | varchar(255) | NO | | | | +| iid | item.id | int(11) | NO | | 0 | | +| parent | | int(11) | NO | | 0 | | +| seen | | tinyint(1) | NO | | 0 | | +| verb | | varchar(255) | NO | | | | +| otype | | varchar(16) | NO | | | | +| name_cache | Cached bbcode parsing of name | tinytext | YES | | NULL | | +| msg_cache | Cached bbcode parsing of msg | mediumtext | YES | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_oembed.md b/doc/database/db_oembed.md new file mode 100644 index 0000000000..5e994eca39 --- /dev/null +++ b/doc/database/db_oembed.md @@ -0,0 +1,10 @@ +Table oembed +============ + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------ | ---------------------------------- | ------------ | ---- | --- | ------------------- | ----- | +| url | page url | varchar(255) | NO | PRI | NULL | | +| content | OEmbed data of the page | text | NO | | NULL | | +| created | datetime of creation | datetime | NO | MUL | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_parsed_url.md b/doc/database/db_parsed_url.md new file mode 100644 index 0000000000..ada42c2ea6 --- /dev/null +++ b/doc/database/db_parsed_url.md @@ -0,0 +1,12 @@ +Table parsed_url +================ + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------ | ---------------------------------- | ------------ | ---- | --- | ------------------- | ----- | +| url | page url | varchar(255) | NO | PRI | NULL | | +| guessing | is the "guessing" mode active? | tinyint(1) | NO | PRI | 0 | | +| oembed | is the data the result of oembed? | tinyint(1) | NO | PRI | 0 | | +| content | page data | text | NO | | NULL | | +| created | datetime of creation | datetime | NO | MUL | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_pconfig.md b/doc/database/db_pconfig.md new file mode 100644 index 0000000000..a4ed10a359 --- /dev/null +++ b/doc/database/db_pconfig.md @@ -0,0 +1,12 @@ +Table pconfic +============= + +| Field | Description | Type | Null | Key | Default | Extra | +|-------|-------------|------------|------|-----|---------|----------------| +| id | | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | MUL | 0 | | +| cat | | char(255) | NO | | | | +| k | | char(255) | NO | | | | +| v | | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_photo.md b/doc/database/db_photo.md new file mode 100644 index 0000000000..2d5bf0938e --- /dev/null +++ b/doc/database/db_photo.md @@ -0,0 +1,29 @@ +Table photo +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ----------- | ------------------------------------------------------ | ---------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | user.id of the owner of this data | int(10) unsigned | NO | MUL | 0 | | +| contact-id | contact.id | int(10) unsigned | NO | | 0 | | +| guid | A unique identifier for this photo | varchar(64) | NO | MUL | | | +| resource-id | | varchar(255) | NO | MUL | | | +| created | creation date | datetime | NO | | 0000-00-00 00:00:00 | | +| edited | last edited date | datetime | NO | | 0000-00-00 00:00:00 | | +| title | | varchar(255) | NO | | | | +| desc | | text | NO | | NULL | | +| album | The name of the album to which the photo belongs | varchar(255) | NO | | | | +| filename | | varchar(255) | NO | | | | +| type | image type | varchar(128) | NO | | image/jpeg | | +| height | | smallint(6) | NO | | 0 | | +| width | | smallint(6) | NO | | 0 | | +| size | | int(10) unsigned | NO | | 0 | | +| data | | mediumblob | NO | | NULL | | +| scale | | tinyint(3) | NO | | 0 | | +| profile | | tinyint(1) | NO | | 0 | | +| allow_cid | Access Control - list of allowed contact.id '<19><78>' | mediumtext | NO | | NULL | | +| allow_gid | Access Control - list of allowed groups | mediumtext | NO | | NULL | | +| deny_cid | Access Control - list of denied contact.id | mediumtext | NO | | NULL | | +| deny_gid | Access Control - list of denied groups | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_poll.md b/doc/database/db_poll.md new file mode 100644 index 0000000000..fd89b345c1 --- /dev/null +++ b/doc/database/db_poll.md @@ -0,0 +1,19 @@ +Table poll +========== + +| Field | Description | Type | Null | Key | Default | Extra | +|-------|-------------|------------|------|-----|---------|----------------| +| id | | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | MUL | 0 | | +| q0 | | mediumtext | NO | | NULL | | +| q1 | | mediumtext | NO | | NULL | | +| q2 | | mediumtext | NO | | NULL | | +| q3 | | mediumtext | NO | | NULL | | +| q4 | | mediumtext | NO | | NULL | | +| q5 | | mediumtext | NO | | NULL | | +| q6 | | mediumtext | NO | | NULL | | +| q7 | | mediumtext | NO | | NULL | | +| q8 | | mediumtext | NO | | NULL | | +| q9 | | mediumtext | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_poll_result.md b/doc/database/db_poll_result.md new file mode 100644 index 0000000000..5a732d0adf --- /dev/null +++ b/doc/database/db_poll_result.md @@ -0,0 +1,10 @@ +Table poll_result +================= + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|---------|------|-----|---------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| poll_id | | int(11) | NO | MUL | 0 | | +| choice | | int(11) | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_profile.md b/doc/database/db_profile.md new file mode 100644 index 0000000000..fba351a2d9 --- /dev/null +++ b/doc/database/db_profile.md @@ -0,0 +1,48 @@ +Table profile +============= + +| Field | Description | Type | Null | Key | Default | Extra | +|--------------|-----------------------------------------------|--------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | user.id of the owner of this data | int(11) | NO | | 0 | | +| profile-name | Name of the profile | varchar(255) | NO | | | | +| is-default | Mark this profile as default profile | tinyint(1) | NO | | 0 | | +| hide-friends | Hide friend list from viewers of this profile | tinyint(1) | NO | | 0 | | +| name | | varchar(255) | NO | | | | +| pdesc | Title or description | varchar(255) | NO | | | | +| dob | Day of birth | varchar(32) | NO | | 0000-00-00 | | +| address | | varchar(255) | NO | | | | +| locality | | varchar(255) | NO | | | | +| region | | varchar(255) | NO | | | | +| postal-code | | varchar(32) | NO | | | | +| country-name | | varchar(255) | NO | | | | +| hometown | | varchar(255) | NO | MUL | | | +| gender | | varchar(32) | NO | | | | +| marital | | varchar(255) | NO | | | | +| with | | text | NO | | NULL | | +| howlong | | datetime | NO | | 0000-00-00 00:00:00 | | +| sexual | | varchar(255) | NO | | | | +| politic | | varchar(255) | NO | | | | +| religion | | varchar(255) | NO | | | | +| pub_keywords | | text | NO | | NULL | | +| prv_keywords | | text | NO | | NULL | | +| likes | | text | NO | | NULL | | +| dislikes | | text | NO | | NULL | | +| about | | text | NO | | NULL | | +| summary | | varchar(255) | NO | | | | +| music | | text | NO | | NULL | | +| book | | text | NO | | NULL | | +| tv | | text | NO | | NULL | | +| film | | text | NO | | NULL | | +| interest | | text | NO | | NULL | | +| romance | | text | NO | | NULL | | +| work | | text | NO | | NULL | | +| education | | text | NO | | NULL | | +| contact | | text | NO | | NULL | | +| homepage | | varchar(255) | NO | | | | +| photo | | varchar(255) | NO | | | | +| thumb | | varchar(255) | NO | | | | +| publish | publish default profile in local directory | tinyint(1) | NO | | 0 | | +| net-publish | publish profile in global directory | tinyint(1) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_profile_check.md b/doc/database/db_profile_check.md new file mode 100644 index 0000000000..411da06d30 --- /dev/null +++ b/doc/database/db_profile_check.md @@ -0,0 +1,13 @@ +Table profile_check +=================== + +| Field | Description | Type | Null | Key | Default | Extra | +| -------- | ------------- | ---------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| uid | user.id | int(10) unsigned | NO | | 0 | | +| cid | contact.id | int(10) unsigned | NO | | 0 | | +| dfrn_id | | varchar(255) | NO | | | | +| sec | | varchar(255) | NO | | 0 | | +| expire | | int(11) | NO | | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_push_subscriber.md b/doc/database/db_push_subscriber.md new file mode 100644 index 0000000000..b766ccb46c --- /dev/null +++ b/doc/database/db_push_subscriber.md @@ -0,0 +1,10 @@ +Table push_subscriber +===================== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|---------|------|-----|---------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| poll_id | | int(11) | NO | MUL | 0 | | +| choice | | int(11) | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_queue.md b/doc/database/db_queue.md new file mode 100644 index 0000000000..44bb69c2ec --- /dev/null +++ b/doc/database/db_queue.md @@ -0,0 +1,14 @@ +Table queue +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +|---------|------------------|-------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| cid | | int(11) | NO | MUL | 0 | | +| network | | varchar(32) | NO | MUL | | | +| created | | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| last | | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| content | | mediumtext | NO | | NULL | | +| batch | | tinyint(1) | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_register.md b/doc/database/db_register.md new file mode 100644 index 0000000000..ac2769b900 --- /dev/null +++ b/doc/database/db_register.md @@ -0,0 +1,13 @@ +Table register +============== + +| Field | Description | Type | Null | Key | Default | Extra | +| -------- | ------------- | ---------------- | ---- | --- | ------------------- | --------------- | +| id | sequential ID | int(11) unsigned | NO | PRI | NULL | auto_increment | +| hash | | varchar(255) | NO | | | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | +| uid | user.id | int(11) unsigned | NO | | | | +| password | | varchar(255) | NO | | | | +| language | | varchar(16) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_search.md b/doc/database/db_search.md new file mode 100644 index 0000000000..63d0a46b40 --- /dev/null +++ b/doc/database/db_search.md @@ -0,0 +1,10 @@ +Table search +============ + +| Field | Description | Type | Null | Key | Default | Extra | +|-------|------------------|--------------|------|-----|---------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | MUL | 0 | | +| term | | varchar(255) | NO | MUL | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_session.md b/doc/database/db_session.md new file mode 100644 index 0000000000..1e87c24d03 --- /dev/null +++ b/doc/database/db_session.md @@ -0,0 +1,11 @@ +Table session +============= + +| Field | Description | Type | Null | Key | Default | Extra | +| ------ | ------------- | ------------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | bigint(20) unsigned | NO | PRI | NULL | auto_increment | +| sid | | varchar(255) | NO | MUL | | | +| data | | text | NO | | NULL | | +| expire | | int(10) unsigned | NO | MUL | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_sign.md b/doc/database/db_sign.md new file mode 100644 index 0000000000..6986613e59 --- /dev/null +++ b/doc/database/db_sign.md @@ -0,0 +1,12 @@ +Table sign +========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ------------ | ------------- | ---------------- | ---- | --- | ------- | --------------- | +| id | sequential ID | int(10) unsigned | NO | PRI | NULL | auto_increment | +| iid | item.id | int(10) unsigned | NO | MUL | 0 | | +| signed_text | | mediumtext | NO | | NULL | | +| signature | | text | NO | | NULL | | +| signer | | varchar(255) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_spam.md b/doc/database/db_spam.md new file mode 100644 index 0000000000..f1428a6821 --- /dev/null +++ b/doc/database/db_spam.md @@ -0,0 +1,13 @@ +Table spam +========== + +| Field | Description | Type | Null | Key | Default | Extra | +| ----- | ----------- | ------------ | ---- | --- | ------------------- | --------------- | +| id | | int(11) | NO | PRI | NULL | auto_increment | +| uid | | int(11) | NO | MUL | 0 | | +| spam | | int(11) | NO | MUL | 0 | | +| ham | | int(11) | NO | MUL | 0 | | +| term | | varchar(255) | NO | MUL | | | +| date | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_term.md b/doc/database/db_term.md new file mode 100644 index 0000000000..0240685a0c --- /dev/null +++ b/doc/database/db_term.md @@ -0,0 +1,19 @@ +Table term +========== + +| Field | Description | Type | Null | Key | Default | Extra | +|----------| ------------- |---------------------|------|-----|---------------------|----------------| +| tid | | int(10) unsigned | NO | PRI | NULL | auto_increment | +| oid | | int(10) unsigned | NO | MUL | 0 | | +| otype | | tinyint(3) unsigned | NO | MUL | 0 | | +| type | | tinyint(3) unsigned | NO | MUL | 0 | | +| term | | varchar(255) | NO | | | | +| url | | varchar(255) | NO | | | | +| aid | | int(10) unsigned | NO | | 0 | | +| uid | | int(10) unsigned | NO | MUL | 0 | | +| guid | | varchar(255) | NO | MUL | | | +| created | | datetime | NO | | 0000-00-00 00:00:00 | | +| received | | datetime | NO | | 0000-00-00 00:00:00 | | +| global | | tinyint(1) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_thread.md b/doc/database/db_thread.md new file mode 100644 index 0000000000..d6484b7609 --- /dev/null +++ b/doc/database/db_thread.md @@ -0,0 +1,33 @@ +Table thread +============ + +| Field | Description | Type | Null | Key | Default | Extra | +|-------------|------------------|------------------|------|-----|---------------------|-------| +| iid | sequential ID | int(10) unsigned | NO | PRI | 0 | | +| uid | | int(10) unsigned | NO | MUL | 0 | | +| contact-id | | int(11) unsigned | NO | | 0 | | +| gcontact-id | Global Contact | int(11) unsigned | NO | | 0 | | +| owner-id | Item owner | int(11) unsigned | NO | MUL | 0 | | +| author-id | Item author | int(11) unsigned | NO | MUL | 0 | | +| created | | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| edited | | datetime | NO | | 0000-00-00 00:00:00 | | +| commented | | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| received | | datetime | NO | | 0000-00-00 00:00:00 | | +| changed | | datetime | NO | | 0000-00-00 00:00:00 | | +| wall | | tinyint(1) | NO | MUL | 0 | | +| private | | tinyint(1) | NO | | 0 | | +| pubmail | | tinyint(1) | NO | | 0 | | +| moderated | | tinyint(1) | NO | | 0 | | +| visible | | tinyint(1) | NO | | 0 | | +| spam | | tinyint(1) | NO | | 0 | | +| starred | | tinyint(1) | NO | | 0 | | +| ignored | | tinyint(1) | NO | | 0 | | +| bookmark | | tinyint(1) | NO | | 0 | | +| unseen | | tinyint(1) | NO | | 1 | | +| deleted | | tinyint(1) | NO | | 0 | | +| origin | | tinyint(1) | NO | | 0 | | +| forum_mode | | tinyint(1) | NO | | 0 | | +| mention | | tinyint(1) | NO | | 0 | | +| network | | varchar(32) | NO | | | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_tokens.md b/doc/database/db_tokens.md new file mode 100644 index 0000000000..3b3778d68c --- /dev/null +++ b/doc/database/db_tokens.md @@ -0,0 +1,13 @@ +Table tokens +============ + +| Field | Description | Type | Null | Key | Default | Extra | +| ---------- | ----------- | ------------ | ---- | --- | ------- | ----- | +| id | | varchar(40) | NO | PRI | NULL | | +| secret | | text | NO | | NULL | | +| client_id | | varchar(20) | NO | | | | +| expires | | int(11) | NO | | 0 | | +| scope | | varchar(200) | NO | | | | +| uid | | int(11) | NO | | 0 | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_user.md b/doc/database/db_user.md new file mode 100644 index 0000000000..21ca211d73 --- /dev/null +++ b/doc/database/db_user.md @@ -0,0 +1,78 @@ +Table user +========== + +| Field | Description | Type | Null | Key | Default | Extra | +|--------------------------|-----------------------------------------------------------------------------------------|---------------------|------|-----|---------------------|----------------| +| uid | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| guid | A unique identifier for this user | varchar(64) | NO | | | | +| username | Name that this user is known by | varchar(255) | NO | | | | +| password | encrypted password | varchar(255) | NO | | | | +| nickname | nick- and user name | varchar(255) | NO | MUL | | | +| email | the users email address | varchar(255) | NO | | | | +| openid | | varchar(255) | NO | | | | +| timezone | PHP-legal timezone | varchar(128) | NO | | | | +| language | default language | varchar(32) | NO | | en | | +| register_date | timestamp of registration | datetime | NO | | 0000-00-00 00:00:00 | | +| login_date | timestamp of last login | datetime | NO | | 0000-00-00 00:00:00 | | +| default-location | Default for item.location | varchar(255) | NO | | | | +| allow_location | 1 allows to display the location | tinyint(1) | NO | | 0 | | +| theme | user theme preference | varchar(255) | NO | | | | +| pubkey | RSA public key 4096 bit | text | NO | | NULL | | +| prvkey | RSA private key 4096 bit | text | NO | | NULL | | +| spubkey | | text | NO | | NULL | | +| sprvkey | | text | NO | | NULL | | +| verified | user is verified through email | tinyint(1) unsigned | NO | | 0 | | +| blocked | 1 for user is blocked | tinyint(1) unsigned | NO | | 0 | | +| blockwall | Prohibit contacts to post to the profile page of the user | tinyint(1) unsigned | NO | | 0 | | +| hidewall | Hide profile details from unkown viewers | tinyint(1) unsigned | NO | | 0 | | +| blocktags | Prohibit contacts to tag the post of this user | tinyint(1) unsigned | NO | | 0 | | +| unkmail | Permit unknown people to send private mails to this user | tinyint(1) | NO | | 0 | | +| cntunkmail | | int(11) | NO | | 10 | | +| notify-flags | email notification options | int(11) unsigned | NO | | 65535 | | +| page-flags | page/profile type | int(11) unsigned | NO | | 0 | | +| prvnets | | tinyint(1) | NO | | 0 | | +| pwdreset | | varchar(255) | NO | | | | +| maxreq | | int(11) | NO | | 10 | | +| expire | | int(11) unsigned | NO | | 0 | | +| account_removed | if 1 the account is removed | tinyint(1) | NO | | 0 | | +| account_expired | | tinyint(1) | NO | | 0 | | +| account_expires_on | timestamp when account expires and will be deleted | datetime | NO | | 0000-00-00 00:00:00 | | +| expire_notification_sent | timestamp of last warning of account expiration | datetime | NO | | 0000-00-00 00:00:00 | | +| service_class | service class for this account, determines what if any limits/restrictions are in place | varchar(32) | NO | | | | +| def_gid | | int(11) | NO | | 0 | | +| allow_cid | default permission for this user | mediumtext | NO | | NULL | | +| allow_gid | default permission for this user | mediumtext | NO | | NULL | | +| deny_cid | default permission for this user | mediumtext | NO | | NULL | | +| deny_gid | default permission for this user | mediumtext | NO | | NULL | | +| openidserver | | text | NO | | NULL | | + +``` +/** +* page-flags +*/ +define ( 'PAGE_NORMAL', 0 ); +define ( 'PAGE_SOAPBOX', 1 ); +define ( 'PAGE_COMMUNITY', 2 ); +define ( 'PAGE_FREELOVE', 3 ); +define ( 'PAGE_BLOG', 4 ); +define ( 'PAGE_PRVGROUP', 5 ); + +/** +* notify-flags +*/ +define ( 'NOTIFY_INTRO', 0x0001 ); +define ( 'NOTIFY_CONFIRM', 0x0002 ); +define ( 'NOTIFY_WALL', 0x0004 ); +define ( 'NOTIFY_COMMENT', 0x0008 ); +define ( 'NOTIFY_MAIL', 0x0010 ); +define ( 'NOTIFY_SUGGEST', 0x0020 ); +define ( 'NOTIFY_PROFILE', 0x0040 ); +define ( 'NOTIFY_TAGSELF', 0x0080 ); +define ( 'NOTIFY_TAGSHARE', 0x0100 ); +define ( 'NOTIFY_POKE', 0x0200 ); +define ( 'NOTIFY_SHARE', 0x0400 ); + +define ( 'NOTIFY_SYSTEM', 0x8000 ); +``` + +Return to [database documentation](help/database) diff --git a/doc/database/db_userd.md b/doc/database/db_userd.md new file mode 100644 index 0000000000..80e3084757 --- /dev/null +++ b/doc/database/db_userd.md @@ -0,0 +1,9 @@ +Table userd +=========== + +| Field | Description | Type | Null | Key | Default | Extra | +|----------|------------------|--------------|------|-----|---------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| username | | varchar(255) | NO | MUL | NULL | | + +Return to [database documentation](help/database) diff --git a/doc/database/db_workerqueue.md b/doc/database/db_workerqueue.md new file mode 100644 index 0000000000..b1f5245aea --- /dev/null +++ b/doc/database/db_workerqueue.md @@ -0,0 +1,13 @@ +Table workerqueue +================= + +| Field | Description | Type | Null | Key | Default | Extra | +|-----------|------------------|---------------------|------|-----|---------------------|----------------| +| id | sequential ID | int(11) | NO | PRI | NULL | auto_increment | +| parameter | | text | NO | | NULL | | +| priority | | tinyint(3) unsigned | NO | | 0 | | +| created | | datetime | NO | MUL | 0000-00-00 00:00:00 | | +| pid | | int(11) | NO | | 0 | | +| executed | | datetime | NO | | 0000-00-00 00:00:00 | | + +Return to [database documentation](help/database) diff --git a/doc/de/BBCode.md b/doc/de/BBCode.md index d3e205f0fa..5dc8f3bb06 100644 --- a/doc/de/BBCode.md +++ b/doc/de/BBCode.md @@ -3,153 +3,616 @@ Referenz der Friendica BBCode Tags * [Zur Startseite der Hilfe](help) -Inline Tags ------ - - -
[b]fett[/b]
: fett - -
[i]kursiv[/i]
: kursiv - -
[u]unterstrichen[/u]
: unterstrichen - -
[s]durchgestrichen[/s]
: durchgestrichen - -
[color=red]rot[/color]
: rot - -
[url=http://www.friendica.com]Friendica[/url]
: Friendica - -
[img]http://friendica.com/sites/default/files/friendika-32.png[/img]
: Immagine/foto - -
[size=xx-small]kleiner Text[/size]
: kleiner Text - -
[size=xx-large]groß Text[/size]
: großer Text - -
[size=20]exakte Textgröße[/size] (Textgröße kann jede Zahl sein, in Pixeln)
: exakte Größe - - - - - - - -Block Tags ------ - -
[code]Code[/code]
- -Code - -

 

- -
[quote]Zitat[/quote]
- -
Zitat
- -

 

- -
[quote=Autor]Der Autor? Ich? Nein, nein, nein...[/quote]
- -Autor hat geschrieben:
Der Autor? Ich? Nein, nein, nein...
- -

 

- -
[center]zentrierter Text[/center]
- -
zentrierter Text
- -

 

- -
Wer überrascht werden möchte sollte nicht weiter lesen.[spoiler]Es gibt ein Happy End.[/spoiler]
- -Wer überrascht werden möchte sollte nicht weiter lesen.
*klicken zum öffnen/schließen* - -(Der Text zweischen dem öffnenden und dem schließenden Teil des spoiler Tags wird nicht angezeigt, bis der Link angeklickt wurde. In dem Fall wird *"Es gibt ein Happy End."* also erst angezeigt, wenn der Spoiler verraten wird.) - -

 

- -**Tabelle** -
[table border=1]
- [tr] 
-   [th]Tabellenzeile[/th]
- [/tr]
- [tr]
-   [td]haben Überschriften[/td]
- [/tr]
-[/table]
- -
Tabellenzeile
haben Überschriften
- -

 

- -**Listen** - -
[list]
- [*] Erstes Listenelement
- [*] Zweites Listenelement
-[/list]
-
    -
  • Erstes Listenelement
    -
  • -
  • Zweites Listenelement
  • -
- -[list] ist Equivalent zu [ul] (unsortierte Liste). - -[ol] kann anstelle von [list] verwendet werden um eine sortierte Liste zu erzeugen: - -
[ol]
- [*] Erstes Listenelement
- [*] Zweites Listenelement
-[/ol]
-
  • Erstes Listenelement
  • Zweites Listenelement
- -Für weitere Optionen von sortierten Listen kann man den Stil der Numerierung der Liste definieren: -
[list=1]
: dezimal - -
[list=i]
: römisch, Kleinbuchstaben - -
[list=I]
: römisch, Großbuchstaben - -
[list=a]
: alphabetisch, Kleinbuchstaben - -
[list=A] 
: alphabethisch, Großbuchstaben - - - - -Einbettung von Inhalten ------- - -Man kann viele Dinge, z.B. Video und Audio Dateine, in Nachrichten einbetten. - -
[video]url[/video]
-
[audio]url[/audio]
- -Wobei die *url* von youtube, vimeo, soundcloud oder einer anderen Seite stammen kann die die oembed oder opengraph Spezifikationen unterstützt. -Außerdem kann *url* die genaue url zu einer ogg Datei sein, die dann per HTML5 eingebunden wird. - -
[url]*url*[/url]
- -Wenn *url* entweder oembed oder opengraph unterstützt wird das eingebettete -Objekt (z.B. ein Dokument von scribd) eingebunden. -Der Titel der Seite mit einem Link zur *url* wird ebenfalls angezeigt. - -Um eine Karte in einen Beitrag einzubinden, muss das *openstreetmap* Addon aktiviert werden. Ist dies der Fall, kann mit - -
[map]Broadway 26, New York[/map]
- -eine Karte von [OpenStreetmap](http://openstreetmap.org) eingebettet werden. Zur Identifikation des Ortes können entweder seine Koordinaten in der Form - -
[map=lat,long]
- -oder eine Adresse in obiger Form verwendet werden. - -Spezielle Tags -------- - -Wenn Du über BBCode Tags in einer Nachricht schreiben möchtest, kannst Du [noparse], [nobb] oder [pre] verwenden um den BBCode Tags vor der Evaluierung zu schützen: - -
[noparse][b]fett[/b][/noparse]
: [b]fett[/b] - +## Inline + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[b]fett[/b]fett
[i]kursiv[/i]kursiv
[u]unterstrichen[/u]unterstrichen
[s]durchgestrichen[/s]durchgestrichen
[o]überstrichen[/o]überstrichen
[color=red]rot[/color]rot
[url=http://www.friendica.com]Friendica[/url]Friendica
[img]http://friendica.com/sites/default/files/friendika-32.png[/img]Immagine/foto
[img=64x32]http://friendica.com/sites/default/files/friendika-32.png[/img]
+
Note: provided height is simply discarded.
[size=xx-small]kleiner Text[/size]kleiner Text
[size=xx-large]großer Text[/size]großer Text
[size=20]exakte Größe[/size] (die Größe kann beliebig in Pixeln gewält werden)exakte Größe
[font=serif]Serife Schriftart[/font]Serife Schriftart
+ +### Links + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[url]http://friendica.com[/url]http://friendica.com
[url=http://friendica.com]Friendica[/url]Friendica
[bookmark]http://friendica.com[/bookmark]

+#^[url]http://friendica.com[/url]

Friendica: http://friendica.com

[bookmark=http://friendica.com]Lesezeichen[/bookmark]

+#^[url=http://friendica.com]Lesezeichen[/url]

+#[url=http://friendica.com]^[/url][url=http://friendica.com]Lesezeichen[/url]

Friendica: Lesezeichen

[url=/posts/f16d77b0630f0134740c0cc47a0ea02a]Diaspora Beitrag mit GUID[/url]Diaspora Beitrag mit GUID
#Friendica#Friendica
@Erwähnung@Erwähnung
acct:account@friendica.host.com (WebFinger)acct:account@friendica.host.com
[mail]user@mail.example.com[/mail]user@mail.example.com
[mail=user@mail.example.com]Eine E-Mail senden[/mail]Eine E-Mail senden
+ +## Blocks + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[p]Ein Absatz mit Text[/p]

Ein Absatz mit Text

Eingebetteter [code]Programmcode[/code] im TextEingebetteter Programmcode im Text
[code]Programmcode
über
mehrere
Zeilen[/code]
Programmcode +über +mehrere +Zeilen
[code=php]function text_highlight($s,$lang)[/code]
  1.  function text_highlight($s,$lang)
[quote]Zitat[/quote]
Zitat
[quote=Autor]Autor? Ich? Nein, niemals...[/quote]Autor hat geschrieben:
Autor? Ich? Nein, niemals...
[center]zentrierter Text[/center]
zentrierter Text
Du solltest nicht weiter lesen, wenn du das Ende des Films nicht vorher erfahren willst. [spoiler]Es gibt ein Happy End.[/spoiler] +
+ Du solltest nicht weiter lesen, wenn du das Ende des Films nicht vorher erfahren willst.
+ Zum öffnen/schließen klicken + +
+
+
[spoiler=Autor]Spoiler Alarm[/spoiler] +
+ Autor hat geschrieben
+ Zum öffnen/schließen klicken + +
+
+
[hr] (horizontale Linie)
+ +### Überschriften + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[h1]Titel 1[/h1]

Titel 1

[h2]Titel 2[/h2]

Titel 2

[h3]Titel 3[/h3]

Titel 3

[h4]Titel 4[/h4]

Titel 4

[h5]Titel 5[/h5]
Titel 5
[h6]Titel 6[/h6]
Titel 6
+ +### Tabellen + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[table]
+  [tr]
+    [th]Kopfzeile 1[/th]
+    [th]Kopfzeile 2[/th]
+    [th]Kopfzeile 2[/th]
+  [/tr]
+  [tr]
+    [td]Zelle 1[/td]
+    [td]Zelle 2[/td]
+    [td]Zelle 3[/td]
+  [/tr]
+  [tr]
+    [td]Zelle 4[/td]
+    [td]Zelle 5[/td]
+    [td]Zelle 6[/td]
+  [/tr]
+[/table]
+ + + + + + + + + + + + + + + + + + +
Kopfzeile 1Kopfzeile 2Kopfzeile 3
Zelle 1Zelle 2Zelle 3
Zelle 4Zelle 5Zelle 6
+
[table border=0] + + + + + + + + + + + + + + + + + + +
Kopfzeile 1Kopfzeile 2Kopfzeile 3
Zelle 1Zelle 2Zelle 3
Zelle 4Zelle 5Zelle 6
+
[table border=1] + + + + + + + + + + + + + + + + + + +
Kopfzeile 1Kopfzeile 2Kopfzeile 3
Zelle 1Zelle 2Zelle 3
Zelle 4Zelle 5Zelle 6
+
+ +### Listen + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[ul]
+  [li] Erstes Listenelement
+  [li] Zweites Listenelement
+[/ul]
+[list]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[ol]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/ol]
+[list=1]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[list=]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[list=i]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[list=I]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[list=a]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
[list=A]
+  [*] Erstes Listenelement
+  [*] Zweites Listenelement
+[/list]
+
    +
  • Erstes Listenelement
  • +
  • Zweites Listenelement
  • +
+
+ +## Einbetten + +Du kannst Videos, Musikdateien und weitere Dinge in Beiträgen einbinden. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[video]url[/video]Wobei die *url* eine URL von youtube, vimeo, soundcloud oder einer anderen Plattform sein kann, die die opengraph Spezifikationen unterstützt.
[video]URL der Videodatei[/video] +[audio]URL der Musikdatei[/audio]Die komplette URL einer ogg/ogv/oga/ogm/webm/mp4/mp3 Datei angeben, diese wird dann mit einem HTML5-Player angezeigt.
[youtube]Youtube URL[/youtube]Youtube Video mittels OEmbed anzeigen. Kann u.U, den Player nicht einbetten.
[youtube]Youtube video ID[/youtube]Youtube-Player im iframe einbinden.
[vimeo]Vimeo URL[/vimeo]Vimeo Video mittels OEmbed anzeigen. Kann u.U, den Player nicht einbetten.
[vimeo]Vimeo video ID[/vimeo]Vimeo-Player im iframe einbinden.
[embed]URL[/embed]OEmbed rich content einbetten.
[iframe]URL[/iframe]General embed, iframe size is limited by the theme size for video players.
[url]*url*[/url]Wenn *url* die OEmbed- oder Opengraph-Spezifikationen unterstützt, wird das Objekt eingebettet (z.B. Dokumente von scribd). + Ansonsten wird der Titel der Seite mit der URL verlinkt.
+ +## Karten + +Das Einbetten von Karten benötigt das "openstreetmap" oder das "Google Maps" Addon. +Wenn keines der Addons aktiv ist, werden stattdeßen die Kordinaten angezeigt- + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
[map]Adresse[/map]Bindet eine Karte ein, auf der die angegebene Adresse zentriert ist.
[map=lat,long]Bindet eine Karte ein, die auf die angegebenen Koordinaten zentriert ist.
[map]Bindet eine Karte ein, die auf die Position des Beitrags zentriert ist.
+ +## Zusammenfassungen für lange Beiträge + +Wenn du deine Beiträge auf anderen Netzwerken von Drittanbietern verbreiten möchtest, z.B. Twitter, könntest du Probleme mit deren Zeichenbegrenzung haben. + +Friendica verwendet einen semi-inelligenten Mechanismus um passende Zusammenfassungen zu erstellen. +Du kannst allerdings auch selbst die Zusammenfassungen erstellen, die auf den unterschiedlichen Netzwerken angezeigt werden. +Um dies zu tun, verwendest du den [abstract]-Tag. + + + + + + + + + + +
BBCodeErgebnis
[abstract]Unglaublich interessant! Muss man gesehen haben! Unbedingt dem Link folgen![/abstract]
+Ich möchte euch eine unglaublich langweilige Geschichte erzählen, die ihr sicherlich niemals hören wolltet.
Auf Twitter würde folgender Text verlffentlicht werden
Unglaublich interessant! Muss man gesehen haben! Unbedingt dem Link folgen!
+Wohingegen auf Friendica folgendes stehen würde
Ich möchte euch eine unglaublich langweilige Geschichte erzählen, die ihr sicherlich niemals hören wolltet.
+ +Wenn du magst, kannst du auch unterschiedliche Zusammenfassungen für die unterschiedlichen Netzwerke verwenden. + + + + + + + + + + +
BBCodeErgebnis
+[abstract]Hey Leute, hier sind meines neuesten Bilder![/abstract]
+[abstract=twit]Hallo liebe Twitter Follower. Wollt ihr meine neuesten Bilder sehen?[/abstract]
+[abstract=apdn]Moin liebe Follower auf ADN. Ich habe einige neue Bilder gemacht, die ich euch gerne zeigen will.[/abstract]
+Heute war ich im Wald unterwegs und habe einige wirklich schöne Bilder gemacht...
Für Twitter und App.net wird Friendica in diesem Fall die speziell definierten Zusammenfassungen Verwenden. Für andere Netzwerke (wie z.B. bei der Verwendung des GNU Social Konnektors zum Veröffentlichen auf deinen GNU Social Account) würde die allgemeine Zusammenfassung verwenden.
+ +Wenn du beispielsweise den "buffer"-Konnektor verwendest um Beiträge nach Facebook und Google+ zu senden, dort aber nicht den gesamten Blogbeitrag posten willst sondern nur einen Anreißer, kannst du dies mit dem [abstract]-Tag realisieren. + +Bei Netzwerken wie Facebook oder Google+, die selbst kein Zeichenlimit haben wird das [abstract]-Element allerdings nicht grundsätzlich verwendet. +Daher müssen diese Netzwerke explizit genannt werden. + + + + + + + + + + +
BBCodeErgebnis
+[abstract]Dieser Tage hatte ich eine ungewöhnliche Begegnung...[/abstract]
+[abstract=goog]Hey liebe Google+ Follower. Habt ich schon meinen neuesten Blog-Beitrag gelesen?[/abstract]
+[abstract=face]Hallo liebe Facebook Freunde. Letztens ist mir etwas wirklich schönes paßiert.[/abstract]
+Als ich die Bilder im Wald aufgenommen habe, hatte ich eine wirklich ungewöhnliche Begegnung...
Auf Google und Facebook würde nun die entsprechende Zusammenfassung verbreitet. Für andere Netzwerke würde die allgemeine Zusammenfassung verwendet werden.
+
Auf Friendica wird weiterhin keine Zusammenfassung angezeigt.
+ +Für Verbindungen zu Netzwerken, zu denen Friendica den HTML Code postet, wie Tumblr, Wordpress oder Pump.io wird das [abstract] Element nicht verwendet. +Bei nativen Verbindungen; das heißt zu z.B. Friendica, Hubzilla, Diaspora oder GNU Social Kontakten; wird der ungekürzte Beitrag übertragen. +Die Instanz des Kontakts kümmert sich um die Darstellung. + +## Special + + + + + + + + + + + + + + + + + + + + + + +
BBCodeErgebnis
Wenn du verhindern möchtest, daß der BBCode in einer Nachricht interpretiert wird, kannst du die [noparse], [nobb] oder [pre] Tag verwenden:
+
    +
  • [noparse][b]fett[/b][/noparse]
  • +
  • [nobb][b]fett[/b][/nobb]
  • +
  • [pre][b]fett[/b][/pre]
  • +
+
[b]fett[/b]
[nosmile] kann verwendet werden um für einen Beitrag das umsetzen von Smilies zu verhindern.
+
+ [nosmile] ;-) :-O +
;-) :-O
Benutzerdefinierte Inline-Styles
+
+[style=text-shadow: 0 0 4px #CC0000;]Du kannst alle CSS-Eigenschaften eines Blocks ändern-[/style]
Du kannst alle CSS-Eigenschaften eines Blocks ändern-
Benutzerdefinierte CSS Klassen
+
+[class=custom]Wenn die vergebene Klasse in den CSS Anweisungen existiert, wird sie angewandt.[/class]
<span class="custom">Wenn die
+vergebene Klasse in den CSS Anweisungen
+existiert,wird sie angewandt.</span>
diff --git a/doc/de/Bugs-and-Issues.md b/doc/de/Bugs-and-Issues.md index 1323b4b9d3..4db1bd6aa3 100644 --- a/doc/de/Bugs-and-Issues.md +++ b/doc/de/Bugs-and-Issues.md @@ -6,7 +6,7 @@ Bugs und Probleme Du solltest jeden Bug und jedes Problem, den/das Du findest, zunächst dem Administrator (oder gegebenenfalls der Support-Seite) Deines Servers melden, statt auf der allgemeinen Bug-Seite. Das erleichtert den Entwicklern ihre Arbeit (z. B. neue Features zu entwickeln), da sie sich nicht mit Fehlern beschäftigen müssen, mit denen sie nichts zu tun haben. -Wenn Du technisch versiert bist oder Dein Knoten keine Support-Seite hat, dann kannst Du den Bug Tracker nutzen. +Wenn Du technisch versiert bist oder Dein Knoten keine Support-Seite hat, dann kannst Du den Bug Tracker nutzen. Bitte durchsuche zunächst die Seite, ob es bereits einen offenen Bug gibt, der Deiner Anfrage entspricht. Liefere so viele Informationen wie möglich zu dem Bug. diff --git a/doc/de/Home.md b/doc/de/Home.md index 44ebfc0285..e3017c0c60 100644 --- a/doc/de/Home.md +++ b/doc/de/Home.md @@ -20,37 +20,37 @@ Friendica - Dokumentation und Ressourcen * [Community-Foren](help/Forums) * [Chats](help/Chats) * Weiterführende Informationen - * [Performance verbessern](help/Improve-Performance) * [Account umziehen](help/Move-Account) * [Account löschen](help/Remove-Account) * [Bugs und Probleme](help/Bugs-and-Issues) * [Häufig gestellte Fragen (FAQ)](help/FAQ) -**Technische Dokumentation** +**Dokumentation für Administratoren** * [Installation](help/Install) -* [Konfigurationen](help/Settings) +* [Konfigurationen & Admin-Panel](help/Settings) * [Plugins](help/Plugins) * [Konnektoren (Connectors) installieren (Twitter/GNU Social)](help/Installing-Connectors) * [Installation eines ejabberd Servers (XMPP-Chat) mit synchronisierten Anmeldedaten](help/install-ejabberd) (EN) -* [Nachrichtenfluss](help/Message-Flow) * [Betreibe deine Seite mit einem SSL-Zertifikat](help/SSL) -* [Entwickler](help/Developers) -* [Twitter/GNU Social API Functions](help/api) (EN) -* [Translation of Friendica](help/translations) (EN) * [Konfigurationswerte, die nur in der .htconfig.php gesetzt werden können](help/htconfig) (EN) +* [Performance verbessern](help/Improve-Performance) -**Entwickler Dokumentation** +**Dokumentation für Entwickler** -* [Where to get started?](help/Developers-Intro) +* [Entwickler](help/Developers) +* [Where to get started?](help/Developers-Intro) (EN) * [Help on Github](help/Github) * [Help on Vagrant](help/Vagrant) -* [How to translate Friendica](help/translations) +* [How to translate Friendica](help/translations) (EN) * [Bugs and Issues](help/Bugs-and-Issues) * [Plugin Development](help/Plugins) * [Theme Development](help/themes) * [Smarty 3 Templates](help/smarty3-templates) +* [Protokoll Dokumentation](help/Protocol) (EN) +* [Datenbank-Schema](help/database) * [Code-Referenz (mit doxygen generiert - setzt Cookies)](doc/html/) +* [Twitter/GNU Social API Functions](help/api) (EN) **Externe Ressourcen** diff --git a/doc/de/Message-Flow.md b/doc/de/Message-Flow.md index 0694db1344..3d4c912ccf 100644 --- a/doc/de/Message-Flow.md +++ b/doc/de/Message-Flow.md @@ -6,7 +6,7 @@ Friendica Nachrichtenfluss Diese Seite soll einige Infos darüber dokumentieren, wie Nachrichten innerhalb von Friendica von einer Person zur anderen übertragen werden. Es gibt verschiedene Pfade, die verschiedene Protokolle und Nachrichtenformate nutzen. -Diejenigen, die den Nachrichtenfluss genauer verstehen wollen, sollten sich mindestens mit dem DFRN-Protokoll (http://dfrn.org/dfrn.pdf) und den Elementen zur Nachrichtenverarbeitung des OStatus Stack informieren (salmon und Pubsubhubbub). +Diejenigen, die den Nachrichtenfluss genauer verstehen wollen, sollten sich mindestens mit dem DFRN-Protokoll ([Dokument mit den DFRN Spezifikationen](https://github.com/friendica/friendica/blob/master/spec/dfrn2.pdf)) und den Elementen zur Nachrichtenverarbeitung des OStatus Stack informieren (salmon und Pubsubhubbub). Der Großteil der Nachrichtenverarbeitung nutzt die Datei include/items.php, welche Funktionen für verschiedene Feed-bezogene Import-/Exportaktivitäten liefert. @@ -24,7 +24,7 @@ PuSh-Feeds (pubsubhubbub) kommen via mod/pubsub.php an. DFRN-poll Feed-Imports kommen via include/poller.php als geplanter Task an, das implementiert die lokale Bearbeitung (local side) des DFRN-Protokolls. -Szenario #1. Bob schreibt eine öffentliche Statusnachricht +### Szenario #1. Bob schreibt eine öffentliche Statusnachricht Dies ist eine öffentliche Nachricht ohne begrenzte Nutzerfreigabe, so dass keine private Übertragung notwendig ist. Es gibt zwei Wege, die genutzt werden können - als bbcode an DFRN-Clients oder als durch den Server konvertierten HTML-Code (mit PuSH; pubsubhubbub). @@ -33,13 +33,13 @@ Sie fallen zurück auf eine tägliche Abfrage, wenn der Hub Übertragungsschwier Wenn kein spezifizierter Hub oder Hubs ausgewählt sind, werden DFRN-Clients in einer pro Kontakt konfigurierbaren Rate mit bis zu 5-Minuten-Intervallen abfragen. Feeds, die via DFRN-Poll abgerufen werden, sind bbcode und können auch private Unterhaltungen enthalten, die vom Poller auf ihre Zugriffsrechte hin geprüft werden. -Szenario #2. Jack antwortet auf Bobs öffentliche Nachricht. Jack ist im Friendica/DFRN-Netzwerk. +### Szenario #2. Jack antwortet auf Bobs öffentliche Nachricht. Jack ist im Friendica/DFRN-Netzwerk. Jack nutzt dfrn-notify, um eine direkte Antwort an Bob zu schicken. Bob erstellt dann einen Feed der Unterhaltung und sendet diesen an jeden, der an der Unterhaltung beteiligt ist und dfrn-notify nutzt. Die PuSH-Hubs werden darüber informiert, dass neuer Inhalt verfügbar ist. Der/die Hub/s erhalten dann die neuesten Feeds und übertragen diese an alle Hub-Teilnehmer (die auch zu verschiedenen Netzwerken gehören können). -Szenario #3. Mary antwortet auf Bobs öffentliche Nachricht. Mary ist im Friendica/DFRN-Netzwerk. +### Szenario #3. Mary antwortet auf Bobs öffentliche Nachricht. Mary ist im Friendica/DFRN-Netzwerk. Mary nutzt dfrn-notify, um eine direkte Antwort an Bob zu schicken. Bob erstellt dann einen Feed der Unterhaltung und sendet diesen an jeden, der an der Unterhaltung beteiligt ist (mit Ausnahme von Bob selbst; die Unterhaltung wird nun an Jack und Mary geschickt). @@ -47,14 +47,14 @@ Die Nachrichten werden mit dfrn-notify übertragen. PuSH-Hubs werden darüber informiert, dass neuer Inhalt verfügbar ist. Der/die Hub/s erhalten dann die neuesten Feeds und übertragen sie an alle Hub-Teilnehmer (die auch zu verschiedenen Netzwerken gehören können). -Szenario #4. William antwortet auf Bobs öffentliche Nachricht. William ist in einem OStatus-Netzwerk. +### Szenario #4. William antwortet auf Bobs öffentliche Nachricht. William ist in einem OStatus-Netzwerk. William nutzt salmon, um Bob über seine Antwort zu benachrichtigen. Der Inhalt ist HTML-Code, der in das Salmon Magic Envelope eingebettet ist. Bob erstellt dann einen Feed der Unterhaltung und sendet es an alle Friendica-Nutzer, die an der Unterhaltung beteiligt sind und dfrn-notify nutzen (mit Ausnahme von William selbst; die Unterhaltung wird an Jack und Mary weitergeleitet). PuSH-Hubs werden darüber informiert, dass neuer Inhalt verfügbar ist. Der/die Hub/s erhalten dann die neuesten Feeds und übertragen sie an alle Hub-Teilnehmer (die auch zu verschiedenen Netzwerken gehören können). -Szenario #5. Bob schreibt eine private Nachricht an Mary und Jack. +### Szenario #5. Bob schreibt eine private Nachricht an Mary und Jack. Die Nachricht wird sofort an Mary und Jack mit Hilfe von dfrn_notify geschickt. Öffentliche Hubs werden nicht benachrichtigt. diff --git a/doc/de/Plugins.md b/doc/de/Plugins.md index dcff41a4b6..8b1c71177d 100644 --- a/doc/de/Plugins.md +++ b/doc/de/Plugins.md @@ -1,27 +1,28 @@ -**Friendica Addon/Plugin-Entwicklung** +Friendica Addon/Plugin-Entwicklung ============== * [Zur Startseite der Hilfe](help) -Bitte schau dir das Beispiel-Addon "randplace" für ein funktionierendes Beispiel für manche der hier aufgeführten Funktionen an. -Das Facebook-Addon bietet ein Beispiel dafür, die "addon"- und "module"-Funktion gemeinsam zu integrieren. -Addons arbeiten, indem sie Event Hooks abfangen. Module arbeiten, indem bestimmte Seitenanfragen (durch den URL-Pfad) abgefangen werden +Bitte schau dir das Beispiel-Addon "randplace" für ein funktionierendes Beispiel für manche der hier aufgeführten Funktionen an. +Das Facebook-Addon bietet ein Beispiel dafür, die "addon"- und "module"-Funktion gemeinsam zu integrieren. +Addons arbeiten, indem sie Event Hooks abfangen. +Module arbeiten, indem bestimmte Seitenanfragen (durch den URL-Pfad) abgefangen werden. -Plugin-Namen können keine Leerstellen oder andere Interpunktionen enthalten und werden als Datei- und Funktionsnamen genutzt. -Du kannst einen lesbaren Namen im Kommentarblock eintragen. -Jedes Addon muss beides beinhalten - eine Installations- und eine Deinstallationsfunktion, die auf dem Addon-/Plugin-Namen basieren; z.B. "plugin1name_install()". -Diese beiden Funktionen haben keine Argumente und sind dafür verantwortlich, Event Hooks zu registrieren und abzumelden (unregistering), die dein Plugin benötigt. -Die Installations- und Deinstallationsfunktionfunktionen werden auch ausgeführt (z.B. neu installiert), wenn sich das Plugin nach der Installation ändert - somit sollte deine Deinstallationsfunktion keine Daten zerstört und deine Installationsfunktion sollte bestehende Daten berücksichtigen. +Plugin-Namen können keine Leerstellen oder andere Interpunktionen enthalten und werden als Datei- und Funktionsnamen genutzt. +Du kannst einen lesbaren Namen im Kommentarblock eintragen. +Jedes Addon muss beides beinhalten - eine Installations- und eine Deinstallationsfunktion, die auf dem Addon-/Plugin-Namen basieren; z.B. "plugin1name_install()". +Diese beiden Funktionen haben keine Argumente und sind dafür verantwortlich, Event Hooks zu registrieren und abzumelden (unregistering), die dein Plugin benötigt. +Die Installations- und Deinstallationsfunktionfunktionen werden auch ausgeführt (z.B. neu installiert), wenn sich das Plugin nach der Installation ändert - somit sollte deine Deinstallationsfunktion keine Daten zerstört und deine Installationsfunktion sollte bestehende Daten berücksichtigen. Zukünftige Extensions werden möglicherweise "Setup" und "Entfernen" anbieten. Plugins sollten einen Kommentarblock mit den folgenden vier Parametern enthalten: - /* - * Name: My Great Plugin - * Description: This is what my plugin does. It's really cool - * Version: 1.0 - * Author: John Q. Public - */ + /* + * Name: My Great Plugin + * Description: This is what my plugin does. It's really cool. + * Version: 1.0 + * Author: John Q. Public + */ Registriere deine Plugin-Hooks während der Installation. @@ -29,45 +30,50 @@ Registriere deine Plugin-Hooks während der Installation. $hookname ist ein String und entspricht einem bekannten Friendica-Hook. -$file steht für den Pfadnamen, der relativ zum Top-Level-Friendicaverzeichnis liegt. +$file steht für den Pfadnamen, der relativ zum Top-Level-Friendicaverzeichnis liegt. Das *sollte* "addon/plugin_name/plugin_name.php' sein. $function ist ein String und der Name der Funktion, die ausgeführt wird, wenn der Hook aufgerufen wird. +Argumente +--- + Deine Hook-Callback-Funktion wird mit mindestens einem und bis zu zwei Argumenten aufgerufen - function myhook_function(&$a, &$b) { + function myhook_function(App $a, &$b) { } -Wenn du Änderungen an den aufgerufenen Daten vornehmen willst, musst du diese als Referenzvariable (mit "&") während der Funktionsdeklaration deklarieren. +Wenn du Änderungen an den aufgerufenen Daten vornehmen willst, musst du diese als Referenzvariable (mit "&") während der Funktionsdeklaration deklarieren. -$a ist die Friendica "App"-Klasse, die eine Menge an Informationen über den aktuellen Friendica-Status beinhaltet, u.a. welche Module genutzt werden, Konfigurationsinformationen, Inhalte der Seite zum Zeitpunkt des Hook-Aufrufs. -Es ist empfohlen, diese Funktion "$a" zu nennen, um seine Nutzung an den Gebrauch an anderer Stelle anzugleichen. +$a ist die Friendica "App"-Klasse, die eine Menge an Informationen über den aktuellen Friendica-Status beinhaltet, u.a. welche Module genutzt werden, Konfigurationsinformationen, Inhalte der Seite zum Zeitpunkt des Hook-Aufrufs. +Es ist empfohlen, diese Funktion "$a" zu nennen, um seine Nutzung an den Gebrauch an anderer Stelle anzugleichen. -$b kann frei benannt werden. -Diese Information ist speziell auf den Hook bezogen, der aktuell bearbeitet wird, und beinhaltet normalerweise Daten, die du sofort nutzen, anzeigen oder bearbeiten kannst. -Achte darauf, diese mit "&" zu deklarieren, wenn du sie bearbeiten willst. +$b kann frei benannt werden. +Diese Information ist speziell auf den Hook bezogen, der aktuell bearbeitet wird, und beinhaltet normalerweise Daten, die du sofort nutzen, anzeigen oder bearbeiten kannst. +Achte darauf, diese mit "&" zu deklarieren, wenn du sie bearbeiten willst. -**Module** +Module +--- -Plugins/Addons können auch als "Module" agieren und alle Seitenanfragen für eine bestimte URL abfangen. -Um ein Plugin als Modul zu nutzen, ist es nötig, die Funktion "plugin_name_module()" zu definieren, die keine Argumente benötigt und nichts weiter machen muss. +Plugins/Addons können auch als "Module" agieren und alle Seitenanfragen für eine bestimte URL abfangen. +Um ein Plugin als Modul zu nutzen, ist es nötig, die Funktion "plugin_name_module()" zu definieren, die keine Argumente benötigt und nichts weiter machen muss. -Wenn diese Funktion existiert, wirst du nun alle Seitenanfragen für "http://my.web.site/plugin_name" erhalten - mit allen URL-Komponenten als zusätzliche Argumente. -Diese werden in ein Array $a->argv geparst und stimmen mit $a->argc überein, wobei sie die Anzahl der URL-Komponenten abbilden. -So würde http://my.web.site/plugin/arg1/arg2 nach einem Modul "plugin" suchen und seiner Modulfunktion die $a-App-Strukur übergeben (dies ist für viele Komponenten verfügbar). Das umfasst: +Wenn diese Funktion existiert, wirst du nun alle Seitenanfragen für "http://example.com/plugin_name" erhalten - mit allen URL-Komponenten als zusätzliche Argumente. +Diese werden in ein Array $a->argv geparst und stimmen mit $a->argc überein, wobei sie die Anzahl der URL-Komponenten abbilden. +So würde http://example.com/plugin/arg1/arg2 nach einem Modul "plugin" suchen und seiner Modulfunktion die $a-App-Strukur übergeben (dies ist für viele Komponenten verfügbar). Das umfasst: - $a->argc = 3 - $a->argv = array(0 => 'plugin', 1 => 'arg1', 2 => 'arg2'); + $a->argc = 3 + $a->argv = array(0 => 'plugin', 1 => 'arg1', 2 => 'arg2'); -Deine Modulfunktionen umfassen oft die Funktion plugin_name_content(&$a), welche den Seiteninhalt definiert und zurückgibt. -Sie können auch plugin_name_post(&$a) umfassen, welches vor der content-Funktion aufgerufen wird und normalerweise die Resultate der POST-Formulare handhabt. -Du kannst ebenso plugin_name_init(&$a) nutzen, was oft frühzeitig aufgerufen wird und das Modul initialisert. +Deine Modulfunktionen umfassen oft die Funktion plugin_name_content(App $a), welche den Seiteninhalt definiert und zurückgibt. +Sie können auch plugin_name_post(App $a) umfassen, welches vor der content-Funktion aufgerufen wird und normalerweise die Resultate der POST-Formulare handhabt. +Du kannst ebenso plugin_name_init(App $a) nutzen, was oft frühzeitig aufgerufen wird und das Modul initialisert. -**Derzeitige Hooks:** +Derzeitige Hooks +--- **'authenticate'** - wird aufgerufen, wenn sich der User einloggt. $b ist ein Array @@ -180,6 +186,9 @@ Du kannst ebenso plugin_name_init(&$a) nutzen, was oft frühzeitig aufgerufen wi - wird aufgerufen nachdem in include/nav,php der Inhalt des Navigations Menüs erzeugt wurde. - $b ist ein Array, das $nav wiederspiegelt. +Komplette Liste der Hook-Callbacks +--- + Eine komplette Liste aller Hook-Callbacks mit den zugehörigen Dateien (am 14-Feb-2012 generiert): Bitte schau in die Quellcodes für Details zu Hooks, die oben nicht dokumentiert sind. boot.php: call_hooks('login_hook',$o); @@ -204,7 +213,7 @@ include/text.php: call_hooks('contact_block_end', $arr); include/text.php: call_hooks('smilie', $s); -include/text.php: call_hooks('prepare_body_init', $item); +include/text.php: call_hooks('prepare_body_init', $item); include/text.php: call_hooks('prepare_body', $prep_arr); @@ -302,7 +311,7 @@ mod/photos.php: call_hooks('photo_post_end',intval($item_id)); mod/photos.php: call_hooks('photo_upload_form',$ret); -mod/friendica.php: call_hooks('about_hook', $o); +mod/friendica.php: call_hooks('about_hook', $o); mod/editpost.php: call_hooks('jot_tool', $jotplugins); @@ -359,4 +368,3 @@ mod/cb.php: call_hooks('cb_afterpost'); mod/cb.php: call_hooks('cb_content', $o); mod/directory.php: call_hooks('directory_item', $arr); - diff --git a/doc/de/SSL.md b/doc/de/SSL.md index e9deb21b7b..d1929120a4 100644 --- a/doc/de/SSL.md +++ b/doc/de/SSL.md @@ -5,7 +5,7 @@ Friendica mit SSL nutzen Disclaimer --- -**Dieses Dokument wurde im November 2015 aktualisiert. +**Dieses Dokument wurde im November 2016 aktualisiert. SSL-Verschlüsselung ist sicherheitskritisch. Das bedeutet, dass sich die empfohlenen Einstellungen schnell verändern. Halte deine Installation auf dem aktuellen Stand und verlasse dich nicht darauf, dass dieses Dokument genau so schnell aktualisiert wird, wie sich Technologien verändern!** @@ -45,55 +45,15 @@ Sie installieren es für dich oder haben in der Weboberfläche eine einfache Upl Um Geld zu sparen, kann es sich lohnen, dort auch nachzufragen, ob sie ein anderes Zertifikat, das du selbst beschaffst, für dich installieren würden. Wenn ja, dann lies weiter. -Ein kostenloses StartSSL-Zertifikat besorgen ---- - -StartSSL ist eine Zertifizierungsstelle, die kostenlose Zertifikate ausstellt. -Sie sind für ein Jahr gültig und genügen für unsere Zwecke. - -### Schritt 1: Client-Zertifikat erstellen - -Wenn du dich erstmalig bei StartSSL anmeldest, erhältst du ein Zertifikat, das in deinem Browser installiert wird. -Du brauchst es, um dich bei StartSSL einzuloggen, auch wenn du später wiederkommst. -Dieses Client-Zertifikat hat nichts mit dem SSL-Zertifikat für deinen Server zu tun. - -### Schritt 2: Email-Adresse und Domain validieren - -Um fortzufahren musst du beweisen, dass du die Email-Adresse, die du angegeben hast, und die Domain, für die du das Zertifikat möchtest, besitzt. -Gehe in den "Validation wizard" und fordere einen Bestätigungslink per Mail an. -Dasselbe machst du auch für die Validierung der Domain. - -### Schritt 3: Das Zertifikat bestellen - -Gehe in den "Certificate wizard". -Wähle das Target Webserver. -Bei der ersten Abfrage der Domain gibst du deine Hauptdomain an. -Im nächsten Schritt kannst du eine Subdomain hinzufügen. -Ein Beispiel: Wenn die Adresse der Friendica-Instanz friendica.beispiel.net lautet, gibst du zuerst beispiel.net an und danach friendica. - -Wenn du weißt, wie man einen openssl-Schlüssel und einen Certificate Signing Request (CSR) erstellt, tu das. -Kopiere den CSR in den Browser, um ihn von StartSSL signiert zu bekommen. - -Wenn du nicht weißt, wie man Schlüssel und CSR erzeugt, nimm das Angebot von StartSSL an, beides für dich zu generieren. -Das bedeutet: StartSSL hat den Schlüssel zu deiner SSL-Verschlüsselung, aber das ist immer noch besser als gar kein Zertifikat. -Lade dein Zertifikat von der Website herunter. -(Oder im zweiten Fall: Lade Zertifikat und Schlüssel herunter.) - -Um dein Zertifikat auf einem Webserver zu installieren, brauchst du noch ein oder zwei andere Dateien: sub.class1.server.ca.pem und ca.pem, auch von StartSSL. -Gehe in die Rubrik "Tool box" und lade "Class 1 Intermediate Server CA" und "StartCom Root CA (PEM encoded)" herunter. - -Wenn du dein Zertifikat zu deinem Hosting-Provider schicken möchtest, brauchen Sie mindestens Zertifikat und Schlüssel. -Schick zur Sicherheit alle vier Dateien hin. -**Du solltest sie auf einem verschlüsselten Weg hinschicken!** - -Wenn du deinen eigenen Server betreibst, lade die Dateien hoch und besuche das Mozilla-Wiki (Link unten). Let's encrypt --- Wenn du einen eigenen Server betreibst und den Nameserver kontrollierst, könnte auch die Initiative "Let's encrypt" interessant für dich werden. -Momentan ist deren Angebot noch nicht fertig. -Auf der [Website](https://letsencrypt.org/) kannst du dich über den Stand informieren. +Sie bietet nicht nur freie SSL Zertifikate sondern auch einen automatisierten Prozess zum Erneuern der Zertifikate. +Um letsencrypt Zertifikate verwenden zu können, musst du dir einen Client auf deinem Server installieren. +Eine Anleitung zum offiziellen Client findet du [hier](https://certbot.eff.org/). +Falls du dir andere Clients anschauen willst, kannst du einen Blick in diese [Liste von alternativen letsencrypt Clients](https://letsencrypt.org/docs/client-options/). Webserver-Einstellungen --- diff --git a/doc/de/Settings.md b/doc/de/Settings.md index 988b3657c0..2b7e89a52c 100644 --- a/doc/de/Settings.md +++ b/doc/de/Settings.md @@ -1,234 +1,76 @@ -Konfigurationen -============== +# Settings * [Zur Startseite der Hilfe](help) -Hier findest du einige eingebaute Features, welche kein graphisches Interface haben oder nicht dokumentiert sind. -Konfigurationseinstellungen sind in der Datei ".htconfig.php" gespeichert. -Bearbeite diese Datei, indem du sie z.B. mit einem Texteditor öffnest. -Verschiedene Systemeinstellungen sind bereits in dieser Datei dokumentiert und werden hier nicht weiter erklärt. +Wenn du der Administrator einer Friendica Instanz bist, hast du Zugriff auf das so genannte **Admin Panel** in dem du die Friendica Instanz konfigurieren kannst, -**Tastaturbefehle** +Auf der Startseite des Admin Panels werden die Informationen zu der Instanz zusammengefasst. +Diese Informationen beinhalten die Anzahl der Nachrichten, die sich aktuell in den Warteschlangen befinden. +Hierbei ist die erste Zahl die Zahl der Nachrichten die gerade aktiv verteilt werden. +Diese Zahl sollte sich relativ schnell sinken. +Die zweite Zahl gibt die Anzahl von Nachrichten an, die nicht zugestellt werden konnten. +Die Zustellung wird zu einem späteren Zeitpunkt noch einmal versucht. +Unter dem Punkt "Warteschlange Inspizieren" kannst du einen schnellen Blick auf die zweite Warteschlange werfen. +Solltest du für die Hintergrundprozesse die Worker aktiviert haben, wird eine dritte Zahl angezeigt. +Diese repräsentiert die Anzahl der Aufgaben, die die Worker noch vor sich haben. +Die Aufgaben der Worker sind priorisiert und werden anhand dieser Prioritäten abgearbeitet. -Friendica erfasst die folgenden Tastaturbefehle: +Des weiteren findest du eine Übersicht über die Accounts auf dem Friendica Knoten, die unter dem Punkt "Nutzer" moderiert werden können. +Sowie eine Liste der derzeit aktivierten Addons. +Diese Liste ist verlinkt, so dass du schnellen Zugriff auf die Informationsseiten der einzelnen Addons hast. +Abschließend findest du auf der Startseite des Admin Panels die installierte Version von Friendica. +Wenn du in Kontakt mit den Entwicklern trittst und Probleme oder Fehler zu schildern, gib diese Version bitte immer mit an. -* [Pause] - Pausiert die Update-Aktivität via "Ajax". Das ist ein Prozess, der Updates durchführt, ohne die Seite neu zu laden. Du kannst diesen Prozess pausieren, um deine Netzwerkauslastung zu reduzieren und/oder um es in der Javascript-Programmierung zum Debuggen zu nutzen. Ein Pausenzeichen erscheint unten links im Fenster. Klicke die [Pause]-Taste ein weiteres Mal, um die Pause zu beenden. +Die Unterabschnitte des Admin Panels kannst du in der Seitenleiste auswählen. -* [F8] - Zeigt eine Sprachauswahl an +## Seite +In diesem Bereich des Admin Panels findest du die Hauptkonfiguration deiner Friendica Instanz. +Er ist in mehrere Unterabschnitte aufgeteilt, wobei die Grundeinstellungen oben auf der Seite zu finden sind. -**Geburtstagsbenachrichtigung** +Da die meisten Konfigurationsoptionen einen Hilfstext im Admin Panel haben, kann und will dieser Artikel nicht alle Einstellungen abdecken. -Geburtstage erscheinen auf deiner Startseite für alle Freunde, die in den nächsten 6 Tagen Geburtstag haben. -Um deinen Geburtstag für alle sichtbar zu machen, musst du deinen Geburtstag (zumindest Tag und Monat) in dein Standardprofil eintragen. -Es ist nicht notwendig, das Jahr einzutragen. +### Grundeinstellungen -**Konfigurationseinstellungen** - - -**Sprache** - -Systemeinstellung - -Bitte schau dir die Datei util/README an, um Informationen zur Erstellung einer Übersetzung zu erhalten. - -Konfiguriere: -``` -$a->config['system']['language'] = 'name'; -``` - - -**System-Thema (Design)** - -Systemeinstellung - -Wähle ein Thema als Standardsystemdesign (welches vom Nutzer überschrieben werden kann). Das Standarddesign ist "default". - -Konfiguriere: -``` -$a->config['system']['theme'] = 'theme-name'; -``` - - -**Verifiziere SSL-Zertifikate** - -Sicherheitseinstellungen - -Standardmäßig erlaubt Friendica SSL-Kommunikation von Seiten, die "selbstunterzeichnete" SSL-Zertifikate nutzen. -Um eine weitreichende Kompatibilität mit anderen Netzwerken und Browsern zu gewährleisten, empfehlen wir, selbstunterzeichnete Zertifikate **nicht** zu nutzen. -Aber wir halten dich nicht davon ab, solche zu nutzen. SSL verschlüsselt alle Daten zwischen den Webseiten (und für deinen Browser), was dir eine komplett verschlüsselte Kommunikation erlaubt. -Auch schützt es deine Login-Daten vor Datendiebstahl. Selbstunterzeichnete Zertifikate können kostenlos erstellt werden. -Diese Zertifikate können allerdings Opfer eines sogenannten ["man-in-the-middle"-Angriffs](http://de.wikipedia.org/wiki/Man-in-the-middle-Angriff) werden, und sind daher weniger bevorzugt. -Wenn du es wünscht, kannst du eine strikte Zertifikatabfrage einstellen. -Das führt dazu, dass du keinerlei Verbindung zu einer selbstunterzeichneten SSL-Seite erstellen kannst - -Konfiguriere: -``` -$a->config['system']['verifyssl'] = true; -``` - - -**Erlaubte Freunde-Domains** - -Kooperationen/Gemeinschaften/Bildung Erweiterung - -Kommagetrennte Liste von Domains, welche eine Freundschaft mit dieser Seite eingehen dürfen. -Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Domains erlaubt. - -Konfiguriere: -``` -$a->config['system']['allowed_sites'] = "sitea.com, *siteb.com"; -``` - - -**Erlaubte Email-Domains** - -Kooperationen/Gemeinschaften/Bildung Erweiterung - -Kommagetrennte Liste von Domains, welche bei der Registrierung als Part der Email-Adresse erlaubt sind. -Das grenzt Leute aus, die nicht Teil der Gruppe oder Organisation sind. -Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Email-Adressen erlaubt. - -Konfiguriere: -``` -$a->config['system']['allowed_email'] = "sitea.com, *siteb.com"; -``` - -**Öffentlichkeit blockieren** - -Kooperationen/Gemeinschaften/Bildung Erweiterung - -Setze diese Einstellung auf "true" und sperre den öffentlichen Zugriff auf alle Seiten, solange man nicht eingeloggt ist. -Das blockiert die Ansicht von Profilen, Freunden, Fotos, vom Verzeichnis und den Suchseiten. -Ein Nebeneffekt ist, dass Einträge dieser Seite nicht im globalen Verzeichnis erscheinen. -Wir empfehlen, speziell diese Einstellung auszuschalten (die Einstellung ist an anderer Stelle auf dieser Seite erklärt). -Beachte: das ist speziell für Seiten, die beabsichtigen, von anderen Friendica-Netzwerken abgeschottet zu sein. -Unautorisierte Personen haben ebenfalls nicht die Möglichkeit, Freundschaftsanfragen von Seitennutzern zu beantworten. -Die Standardeinstellung steht auf "false". -Verfügbar in Version 2.2 und höher. - -Konfiguriere: -``` -$a->config['system']['block_public'] = true; -``` - - -**Veröffentlichung erzwingen** - -Kooperationen/Gemeinschaften/Bildung Erweiterung - -Standardmäßig können Nutzer selbst auswählen, ob ihr Profil im Seitenverzeichnis erscheint. -Diese Einstellung zwingt alle Nutzer dazu, im Verzeichnis zu erscheinen. -Diese Einstellung kann vom Nutzer nicht deaktiviert werden. Die Standardeinstellung steht auf "false". - -Konfiguriere: -``` -$a->config['system']['publish_all'] = true; -``` - - -**Globales Verzeichnis** - -Kooperationen/Gemeinschaften/Bildung Erweiterung - -Mit diesem Befehl wird die URL eingestellt, die zum Update des globalen Verzeichnisses genutzt wird. -Dieser Befehl ist in der Standardkonfiguration enthalten. -Der nichtdokumentierte Teil dieser Einstellung ist, dass das globale Verzeichnis gar nicht verfügbar ist, wenn diese Einstellung nicht gesetzt wird. -Dies erlaubt eine private Kommunikation, die komplett vom globalen Verzeichnis isoliert ist. - -Konfiguriere: -``` -$a->config['system']['directory'] = 'http://dir.friendi.ca'; -``` - - -**Proxy Konfigurationseinstellung** - -Wenn deine Seite eine Proxy-Einstellung nutzt, musst du diese Einstellungen vornehmen, um mit anderen Seiten im Internet zu kommunizieren. - -Konfiguriere: -``` -$a->config['system']['proxy'] = "http://proxyserver.domain:port"; -$a->config['system']['proxyuser'] = "username:password"; -``` - - -**Netzwerk-Timeout** - -Legt fest, wie lange das Netzwerk warten soll, bevor ein Timeout eintritt. -Der Wert wird in Sekunden angegeben. Standardmäßig ist 60 eingestellt; 0 steht für "unbegrenzt" (nicht empfohlen). - -Konfiguriere: - -``` -$a->config['system']['curl_timeout'] = 60; -``` - - -**Banner/Logo** +#### Banner/Logo Hiermit legst du das Banner der Seite fest. Standardmäßig ist das Friendica-Logo und der Name festgelegt. Du kannst hierfür HTML/CSS nutzen, um den Inhalt zu gestalten und/oder die Position zu ändern, wenn es nicht bereits voreingestellt ist. -Konfiguriere: +#### Systensprache -``` -$a->config['system']['banner'] = 'Meine tolle Webseite'; -``` +Diese Einstellung legt die Standardsprache der Instanz fest. +Sie wird verwendet, wenn es Friendica nicht gelingt die Spracheinstellungen des Besuchers zu erkennen oder diese nicht unterstützt wird. +Nutzer können diese Auswahl in den Einstellungen des Benutzerkontos überschreiben. +Die Friendica Gemeinschaft bietet einige Übersetzungen an, von denen einige mehr andere weniger komplett sind. +Mehr Informationen zum Übersetzungsprozess von Friendica findest du [auf dieser Seite](/help/translations) der Dokumentation. -**Maximale Bildgröße** +#### Systemweites Theme -Maximale Bild-Dateigröße in Byte. Standardmäßig ist 0 gesetzt, was bedeutet, dass kein Limit gesetzt ist. +Hier kann das Theme bestimmt werden, welches standardmäßig zum Anzeigen der Seite verwendet werden soll. +Nutzer können in ihren Einstellungen andere Themes wählen. +Derzeit ist das "duepunto zero" Theme das vorausgewählte Theme. -Konfiguriere: +Für mobile Geräte kannst du ein spezielles Theme wählen, wenn das Standardtheme ungeeignet für mobile Geräte sein sollte. +Das `vier` Theme z.B. unterstützt kleine Anzeigen und benötigt kein zusätzliches mobiles Theme. -``` -$a->config['system']['maximagesize'] = 1000000; -``` +### Registrierung - -**UTF-8 Reguläre Ausdrücke** - -Während der Registrierung werden die Namen daraufhin geprüft, ob sie reguläre UTF-8-Ausdrücke nutzen. -Hierfür wird PHP benötigt, um mit einer speziellen Einstellung kompiliert zu werden, die UTF-8-Ausdrücke benutzt. -Wenn du absolut keine Möglichkeit hast, Accounts zu registrieren, setze den Wert von "no_utf" auf "true". -Standardmäßig ist "false" eingestellt (das bedeutet, dass UTF-8-Ausdrücke unterstützt werden und funktionieren). - -Konfiguriere: - -``` -$a->config['system']['no_utf'] = true; -``` - - -**Prüfe vollständigen Namen** +#### Namen auf Vollständigkeit überprüfen Es kann vorkommen, dass viele Spammer versuchen, sich auf deiner Seite zu registrieren. In Testphasen haben wir festgestellt, dass diese automatischen Registrierungen das Feld "Vollständiger Name" oft nur mit Namen ausfüllen, die kein Leerzeichen beinhalten. Wenn du Leuten erlauben willst, sich nur mit einem Namen anzumelden, dann setze die Einstellung auf "true". Die Standardeinstellung ist auf "false" gesetzt. - -Konfiguriere: - -``` -$a->config['system']['no_regfullname'] = true; -``` - - -**OpenID** + +#### OpenID Unterstützung Standardmäßig wird OpenID für die Registrierung und für Logins genutzt. Wenn du nicht willst, dass OpenID-Strukturen für dein System übernommen werden, dann setze "no_openid" auf "true". Standardmäßig ist hier "false" gesetzt. -Konfiguriere: -``` -$a->config['system']['no_openid'] = true; -``` - - -**Multiple Registrierungen** +#### Unterbinde Mehrfachregistrierung Um mehrfache Seiten zu erstellen, muss sich eine Person mehrfach registrieren können. Deine Seiteneinstellung kann Registrierungen komplett blockieren oder an Bedingungen knüpfen. @@ -237,42 +79,246 @@ Hier ist weiterhin eine Bestätigung notwendig, wenn "REGISTER_APPROVE" ausgewä Wenn du die Erstellung weiterer Accounts blockieren willst, dann setze die Einstellung "block_extended_register" auf "true". Standardmäßig ist hier "false" gesetzt. -Konfiguriere: -``` -$a->config['system']['block_extended_register'] = true; -``` +### Datei hochladen +#### Maximale Bildgröße -**Entwicklereinstellungen** +Maximale Bild-Dateigröße in Byte. Standardmäßig ist 0 gesetzt, was bedeutet, dass kein Limit gesetzt ist. -Diese sind am nützlichsten, um Protokollprozesse zu debuggen oder andere Kommunikationsfehler einzugrenzen. +### Regeln -Konfiguriere: -``` -$a->config['system']['debugging'] = true; -$a->config['system']['logfile'] = 'logfile.out'; -$a->config['system']['loglevel'] = LOGGER_DEBUG; -``` -Erstellt detaillierte Debugging-Logfiles, die in der Datei "logfile.out" gespeichert werden (Datei muss auf dem Server mit Schreibrechten versehen sein). "LOGGER_DEBUG" zeigt eine Menge an Systeminformationen, enthält aber keine detaillierten Daten. -Du kannst ebenfalls "LOGGER_ALL" auswählen, allerdings empfehlen wir dieses nur, wenn ein spezifisches Problem eingegrenzt werden soll. -Andere Log-Level sind möglich, werden aber derzeit noch nicht genutzt. +#### URL des weltweiten Verzeichnisses +Mit diesem Befehl wird die URL eingestellt, die zum Update des globalen Verzeichnisses genutzt wird. +Dieser Befehl ist in der Standardkonfiguration enthalten. +Der nicht dokumentierte Teil dieser Einstellung ist, dass das globale Verzeichnis gar nicht verfügbar ist, wenn diese Einstellung nicht gesetzt wird. +Dies erlaubt eine private Kommunikation, die komplett vom globalen Verzeichnis isoliert ist. -**PHP-Fehler-Logging** +#### Erzwinge Veröffentlichung -Nutze die folgenden Einstellungen, um PHP-Fehler direkt in einer Datei zu erfassen. +Standardmäßig können Nutzer selbst auswählen, ob ihr Profil im Seitenverzeichnis erscheint. +Diese Einstellung zwingt alle Nutzer dazu, im Verzeichnis zu erscheinen. +Diese Einstellung kann vom Nutzer nicht deaktiviert werden. Die Standardeinstellung steht auf "false". -Konfiguriere: -``` -error_reporting(E_ERROR | E_WARNING | E_PARSE ); -ini_set('error_log','php.out'); -ini_set('log_errors','1'); -ini_set('display_errors', '0'); -``` +#### Öffentlichen Zugriff blockieren -Diese Befehle erfassen alle PHP-Fehler in der Datei "php.out" (Datei muss auf dem Server mit Schreibrechten versehen sein). -Nicht deklarierte Variablen werden manchmal mit einem Verweis versehen, weshalb wir empfehlen, "E_NOTICE" und "E_ALL" nicht zu nutzen. -Die Menge an Fehlern, die auf diesem Level gemeldet werden, ist komplett harmlos. -Bitte informiere die Entwickler über alle Fehler, die du in deinen Log-Dateien mit den oben genannten Einstellungen erhältst. -Sie weisen generell auf Fehler in, die bearbeitet werden müssen. -Wenn du eine leere (weiße) Seite erhältst, schau in die PHP-Log-Datei - dies deutet fast immer darauf hin, dass ein Fehler aufgetreten ist. +Aktiviere diese Einstellung um den öffentlichen Zugriff auf alle Seiten zu sperren, solange man nicht eingeloggt ist. +Das blockiert die Ansicht von Profilen, Freunden, Fotos, vom Verzeichnis und den Suchseiten. +Ein Nebeneffekt ist, dass Einträge dieser Seite nicht im globalen Verzeichnis erscheinen. +Wir empfehlen, speziell diese Einstellung auszuschalten (die Einstellung ist an anderer Stelle auf dieser Seite erklärt). +Beachte: das ist speziell für Seiten, die beabsichtigen, von anderen Friendica-Netzwerken abgeschottet zu sein. +Unautorisierte Personen haben ebenfalls nicht die Möglichkeit, Freundschaftsanfragen von Seitennutzern zu beantworten. +Die Standardeinstellung ist deaktiviert. +Verfügbar in Version 2.2 und höher. + +#### Erlaubte Domains für Kontakte + +Kommagetrennte Liste von Domains, welche eine Freundschaft mit dieser Seite eingehen dürfen. +Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Domains erlaubt. + +Mit dieser Option kann man einfach geschlossene Netzwerke, z.B. im schulischen Bereich aufbauen, aus denen nicht mit dem Rest des Netzwerks kommuniziert werden soll. + +#### Erlaubte Domains für E-Mails + +Kommagetrennte Liste von Domains, welche bei der Registrierung als Part der Email-Adresse erlaubt sind. +Das grenzt Leute aus, die nicht Teil der Gruppe oder Organisation sind. +Wildcards werden akzeptiert (Wildcard-Unterstützung unter Windows benötigt PHP5.3) Standardmäßig sind alle gültigen Email-Adressen erlaubt. + +#### Nutzern erlauben das remote_self Flag zu setzen + +Webb du die Option `Nutzern erlauben das remote_self Flag zu setzen` aktivierst, können alle Nutzer Atom Feeds in den erweiterten Einstellungen des Kontakts als "Entferntes Konto" markieren. +Dadurch werden automatisch alle Beiträge dieser Feeds für diesen Nutzer gespiegelt und an die Kontakte bei Friendica verteilt. + +Als Administrator der Friendica Instanz kannst du diese Einstellungen ansonsten nur direkt in der Datenbank vornehmen. +Bevor du das tust solltest du sicherstellen, dass du ein Backup der Datenbank hast und genau weißt was die Änderungen an der Datenbank bewirken, die du vornehmen willst. + +### Erweitert + +#### Proxy Einstellungen + +Wenn deine Seite eine Proxy-Einstellung nutzt, musst du diese Einstellungen vornehmen, um mit anderen Seiten im Internet zu kommunizieren. + +#### Netzwerk Wartezeit + +Legt fest, wie lange das Netzwerk warten soll, bevor ein Timeout eintritt. +Der Wert wird in Sekunden angegeben. Standardmäßig ist 60 eingestellt; 0 steht für "unbegrenzt" (nicht empfohlen). + +#### UTF-8 Reguläre Ausdrücke + +Während der Registrierung werden die Namen daraufhin geprüft, ob sie reguläre UTF-8-Ausdrücke nutzen. +Hierfür wird PHP benötigt, um mit einer speziellen Einstellung kompiliert zu werden, die UTF-8-Ausdrücke benutzt. +Wenn du absolut keine Möglichkeit hast, Accounts zu registrieren, setze diesen Wert auf ja. + +#### SSL Überprüfen + +Standardmäßig erlaubt Friendica SSL-Kommunikation von Seiten, die "selbst unterzeichnete" SSL-Zertifikate nutzen. +Um eine weitreichende Kompatibilität mit anderen Netzwerken und Browsern zu gewährleisten, empfehlen wir, selbst unterzeichnete Zertifikate **nicht** zu nutzen. +Aber wir halten dich nicht davon ab, solche zu nutzen. SSL verschlüsselt alle Daten zwischen den Webseiten (und für deinen Browser), was dir eine komplett verschlüsselte Kommunikation erlaubt. +Auch schützt es deine Login-Daten vor Datendiebstahl. Selbst unterzeichnete Zertifikate können kostenlos erstellt werden. +Diese Zertifikate können allerdings Opfer eines sogenannten ["man-in-the-middle"-Angriffs](http://de.wikipedia.org/wiki/Man-in-the-middle-Angriff) werden, und sind daher weniger bevorzugt. +Wenn du es wünscht, kannst du eine strikte Zertifikatabfrage einstellen. +Das führt dazu, dass du keinerlei Verbindung zu einer selbst unterzeichneten SSL-Seite erstellen kannst + +### Automatisch ein Kontaktverzeichnis erstellen + +### Performance + +### Worker + +### Umsiedeln + +## Nutzer + +In diesem Abschnitt des Admin Panels kannst du die Nutzer deiner Friendica Instanz moderieren. + +Solltest du für **Registrierungsmethode** die Einstellung "Bedarf Zustimmung" gewählt haben, werden hier zu Beginn der Seite neue Registrationen aufgelistet. +Als Administrator kannst du hier die Registration akzeptieren oder ablehnen. + +Unter dem Abschnitt mit den Registrationen werden die aktuell auf der Instanz registrierten Nutzer aufgelistet. +Die Liste kann nach Namen, E-Mail Adresse, Datum der Registration, der letzten Anmeldung oder dem letzten Beitrag und dem Account Typ sortiert werden. +An dieser Stelle kannst du existierende Accounts vom Zugriff auf die Instanz blockieren, sie wieder frei geben oder Accounts endgültig löschen. + +Im letzten Bereich auf der Seite kannst du als Administrator neue Accounts anlegen. +Das Passwort für so eingerichtete Accounts werden per E-Mail an die Nutzer geschickt. + +## Plugins + +Dieser Bereich des Admin Panels dient der Auswahl und Konfiguration der Erweiterungen von Friendica. +Sie müssen in das `/addon` Verzeichnis kopiert werden. +Auf der Seite wird eine Liste der verfügbaren Erweiterungen angezeigt. +Neben den Namen der Erweiterungen wird ein Indikator angezeigt, der anzeigt ob das Addon gerade aktiviert ist oder nicht. + +Wenn du die Erweiterungen aktualisiert die du auf deiner Friendica Instanz nutzt könnte es sein, dass sie neu geladen werden müssen, damit die Änderungen aktiviert werden. +Um diesen Prozess zu vereinfachen gibt es am Anfang der Seite einen Button um alle aktiven Plugins neu zu laden. + +## Themen + +Der Bereich zur Kontrolle der auf der Friendica Instanz verfügbaren Themen funktioniert analog zum Plugins Bereich. +Jedes Theme hat eine extra Seite auf der der aktuelle Status, ein Bildschirmfoto des Themes, zusätzliche Informationen und eventuelle Einstellungen des Themes zu finden sind. +Genau wie Erweiterungen können Themes in der Übersichtsliste oder der Theme-Seite aktiviert bzw. deaktiviert werden. +Um ein Standardtheme für die Instanz zu wählen, benutze bitte die *Seiten* Bereich des Admin Panels. + +## Zusätzliche Features + +Es gibt einige optionale Features in Friendica, die Nutzer benutzen können oder halt nicht. +Zum Beispiel den *dislike* Button oder den *Webeditor* beim Erstellen von neuen Beiträgen. +In diesem Bereich des Admin Panels kannst du die Grundeinstellungen für diese Features festlegen und gegebenenfalls die Entscheidung treffen, dass Nutzer deiner Instanz diese auch nicht mehr ändern können. + +## DB Updates + +Wenn sich die Datenbankstruktur Friendicas ändert werden die Änderungen automatisch angewandt. +Solltest du den Verdacht haben, das eine Aktualisierung fehlgeschlagen ist, kannst du in diesem Bereich des Admin Panels den Status der Aktualisierungen überprüfen. + +## Warteschlange Inspizieren + +Auf der Eingangsseite des Admin Panels werden zwei Zahlen fpr die Warteschlangen angegeben. +Die zweite Zahl steht für die Beiträge, die initial nicht zugestellt werden konnten und später nochmal zugestellt werden sollen. +Sollte diese Zahl durch die Decke brechen, solltest du nachsehen an welchen Kontakt die Zustellung der Beiträge nicht funktioniert. + +Unter dem Menüpunkt "Warteschlange Inspizieren" findest du eine Liste dieser nicht zustellbaren Beiträge. +Diese Liste ist nach dem Empfänger sortiert. +Die Kommunikation zu dem Empfänger kann aus unterschiedlichen Gründen gestört sein. +Der andere Server könnte offline sein, oder gerade einfach nur eine hohe Systemlast aufweisen. + +Aber keine Panik! +Friendica wird die Beiträge nicht für alle Zeiten in der Warteschlange behalten. +Nach einiger Zeit werden Knoten als inaktiv identifiziert und Nachrichten an Nutzer dieser Knoten aus der Warteschlange gelöscht. + +## Federation Statistik + +Deine Instanz ist ein Teil eines Netzwerks von Servern dezentraler sozialer Netzwerke, der sogenannten **Federation**. +In diesem Bereich des Admin Panels findest du ein paar Zahlen zu dem Teil der Federation, die deine Instanz kennt. + +## Plugin Features + +Einige der Erweiterungen von Friendica benötigen global gültige Einstellungen, die der Administrator vornehmen muss. +Diese Erweiterungen sind hier aufgelistet, damit du die Einstellungen schneller findest. + +## Protokolle + +Dieser Bereich des Admin Panels ist auf zwei Seiten verteilt. +Die eine Seite dient der Konfiguration, die andere dem Anzeigen der Logs. + +Du solltest die Logdatei nicht in einem Verzeichnis anlegen, auf das man vom Internet aus zugreifen kann. +Wenn du das dennoch tun musste und die Standardeinstellungen des Apache Servers verwendest, dann solltest du darauf achten, dass die Logdateien mit der Endung `.log` oder `.out` enden. +Solltest du einen anderen Webserver verwenden, solltest du sicherstellen, dass der Zugrif zu Dateien mit diesen Endungen nicht möglich ist. + +Es gibt fünf Level der Ausführlichkeit mit denen Friendica arbeitet: Normal, Trace, Debug, Data und All. +Normalerweise solltest du für den Betrieb deiner Friendica Instanz keine Logs benötigen. +Wenn du versuchst einem Problem auf den Grund zu gehen, solltest du das "DEBUG" Level wählen. +Mit dem "All" Level schreibt Friendica alles in die Logdatei. +Die Datenmenge der geloggten Daten kann relativ schnell anwachsen, deshalb empfehlen wir das Anlegen von Protokollen nur zu aktivieren wenn es unbedingt nötig ist. + +**Die Größe der Logdateien kann schnell anwachsen**. +Du solltest deshalb einen Dienst zur [log rotation](https://en.wikipedia.org/wiki/Log_rotation) einrichten. + +**Bekannte Probleme**: Der Dateiname `friendica.log` kann bei speziellen Server Konfigurationen zu Problemen führen (siehe [issue 2209](https://github.com/friendica/friendica/issues/2209)). + +Normalerweise werden Fehler- und Warnmeldungen von PHP unterdrückt. +Wenn du sie aktivieren willst, musst du folgendes in der `.htconfig.php` Datei eintragen um die Meldungen in die Datei `php.out` zu speichern + + error_reporting(E_ERROR | E_WARNING | E_PARSE ); + ini_set('error_log','php.out'); + ini_set('log_errors','1'); + ini_set('display_errors', '0'); + +Die Datei `php.out` muss vom Webserver schreibbar sein und sollte ebenfalls außerhalb der Webverzeichnisse liegen. +Es kommt gelegentlich vor, dass nicht deklarierte Variablen referenziert werden, dehalb raten wir davon ab `E_NOTICE` oder `E_ALL` zu verwenden. +Die überwiegende Mehrzahl der auf diesen Stufen dokumentierten Fehler sind absolut harmlos. +Solltest du mit den oben empfohlenen Einstellungen Fehler finden, teile sie bitte den Entwicklern mit. +Im Allgemeinen sind dies Fehler, die behoben werden sollten. + +Solltest du eine leere (weiße) Seite vorfinden, während du Friendica nutzt, werfe bitte einen Blick in die PHP Logs. +Solche *White Screens* sind so gut wie immer ein Zeichen dafür, dass ein Fehler aufgetreten ist. + +## Diagnose + +In diesem Bereich des Admin Panels findest du zwei Werkzeuge mit der du untersuchen kannst, wie Friendica bestimmte Ressourcen einschätzt. +Diese Werkzeuge sind insbesondere bei der Analyse von Kommunikationsproblemen hilfreich. + +"Adresse untersuchen" zeigt Informationen zu einer URL an, wie Friendica sie wahrnimmt. + +Mit dem zweiten Werkzeug "Webfinger überprüfen" kannst du Informationen zu einem Ding anfordern, das über einen Webfinger ( jemand@example.com ) identifiziert wird. + +# Die Ausnahmen der Regel + +Für die oben genannte Regel gibt es vier Ausnahmen, deren Konfiguration nicht über das Admin Panel vorgenommen werden kann. +Dies sind die Datenbank Einstellungen, die Administrator Accounts, der PHP Pfad und die Konfiguration einer eventuellen Installation in ein Unterverzeichnis unterhalb der Hauptdomain. + +## Datenbank Einstellungen + +Mit den folgenden Einstellungen kannst du die Zugriffsdaten für den Datenbank Server festlegen. + + $db_host = 'your.db.host'; + $db_user = 'db_username'; + $db_pass = 'db_password'; + $db_data = 'database_name'; + +## Administratoren + +Du kannst einen, oder mehrere Accounts, zu Administratoren machen. +Normalerweise trifft dies auf den ersten Account zu, der nach der Installation angelegt wird. +Die Liste der E-Mail Adressen kann aber einfach erweitert werden. +Mit keiner der angegebenen E-Mail Adressen können weitere Accounts registriert werden. + + $a->config['admin_email'] = 'you@example.com, buddy@example.com'; + +## PHP Pfad + +Einige Prozesse von Friendica laufen im Hintergrund. +Für diese Prozesse muss der Pfad zu der PHP Version gesetzt sein, die verwendet werden soll. + + $a->config['php_path'] = '/pfad/zur/php-version'; + +## Unterverzeichnis Konfiguration + +Man kann Friendica in ein Unterverzeichnis des Webservers installieren. +Wir raten allerdings dringen davon ab, da es die Interoperabilität mit anderen Netzwerken (z.B. Diaspora, GNU Social, Hubzilla) verhindert. +Mal angenommen, du hast ein Unterverzeichnis tests und willst Friendica in ein weiteres Unterverzeichnis installieren, dann lautet die Konfiguration hierfür: + + $a->path = 'tests/friendica'; + +## Weitere Ausnahmen + +Es gibt noch einige experimentelle Einstellungen, die nur in der ``.htconfig.php`` Datei konfiguriert werden können. +Im [Konfigurationswerte, die nur in der .htconfig.php gesetzt werden können (EN)](help/htconfig) Artikel kannst du mehr darüber erfahren. diff --git a/doc/de/Text_editor.md b/doc/de/Text_editor.md index 0d9fbb5c74..33fc104dff 100644 --- a/doc/de/Text_editor.md +++ b/doc/de/Text_editor.md @@ -52,6 +52,8 @@ Cleanzero cleanzero.png (inkl. smoothly, testbubble) +Frio frio.png + Frost frost.png Vier vier.png (inkl. dispy) diff --git a/doc/htconfig.md b/doc/htconfig.md index 4764c287c8..a7dd59d1f5 100644 --- a/doc/htconfig.md +++ b/doc/htconfig.md @@ -1,100 +1,115 @@ Config values that can only be set in .htconfig.php =================================================== -There are some config values that haven't found their way into the administration page. This has several reasons. Maybe they are part of a -current development that isn't considered stable and will be added later in the administration page when it is considered safe. Or it triggers -something that isn't expected to be of public interest. Or it is for testing purposes only. +* [Home](help) -**Attention:** Please be warned that you shouldn't use one of these values without the knowledge what it could trigger. Especially don't do that with -undocumented values. +There are some config values that haven't found their way into the administration page. +This has several reasons. +Maybe they are part of a current development that isn't considered stable and will be added later in the administration page when it is considered safe. +Or it triggers something that isn't expected to be of public interest. Or it is for testing purposes only. -The header of the section describes the category, the value is the parameter. Example: To set the directory value please add this -line to your .htconfig.php: +**Attention:** Please be warned that you shouldn't use one of these values without the knowledge what it could trigger. +Especially don't do that with undocumented values. + +The header of the section describes the category, the value is the parameter. +Example: To set the directory value please add this line to your .htconfig.php: $a->config['system']['directory'] = 'http://dir.friendi.ca'; +## jabber ## +* **debug** (Boolean) - Enable debug level for the jabber account synchronisation. +* **logfile** - Logfile for the jabber account synchronisation. +## system ## -## Jabber ## -* debug (Boolean) - Enable debug level for the jabber account synchronisation. -* logfile - Logfile for the jabber account synchronisation. - -## System ## - -* birthday_input_format - Default value is "ymd". -* block_local_dir (Boolean) - Blocks the access to the directory of the local users. -* default_service_class - -* delivery_batch_count - Number of deliveries per process. Default value is 1. (Disabled when using the worker) -* diaspora_test (Boolean) - For development only. Disables the message transfer. -* directory - The path to global directory. If not set then "http://dir.friendi.ca" is used. -* disable_email_validation (Boolean) - Disables the check if a mail address is in a valid format and can be resolved via DNS. -* disable_url_validation (Boolean) - Disables the DNS lookup of an URL. -* event_input_format - Default value is "ymd". -* ignore_cache (Boolean) - For development only. Disables the item cache. -* like_no_comment (Boolean) - Don't update the "commented" value of an item when it is liked. -* local_block (Boolean) - Used in conjunction with "block_public". -* local_search (Boolean) - Blocks the search for not logged in users to prevent crawlers from blocking your system. -* max_contact_queue - Default value is 500. -* max_batch_queue - Default value is 1000. -* no_oembed (Boolean) - Don't use OEmbed to fetch more information about a link. -* no_oembed_rich_content (Boolean) - Don't show the rich content (e.g. embedded PDF). -* no_smilies (Boolean) - Don't show smilies. -* no_view_full_size (Boolean) - Don't add the link "View full size" under a resized image. -* optimize_items (Boolean) - Triggers an SQL command to optimize the item table before expiring items. -* ostatus_poll_timeframe - Defines how old an item can be to try to complete the conversation with it. -* paranoia (Boolean) - Log out users if their IP address changed. -* permit_crawling (Boolean) - Restricts the search for not logged in users to one search per minute. -* free_crawls - Number of "free" searches when "permit_crawling" is activated (Default value is 10) -* crawl_permit_period - Period in seconds between allowed searches when the number of free searches is reached and "permit_crawling" is activated (Default value is 60) -* png_quality - Default value is 8. -* proc_windows (Boolean) - Should be enabled if Friendica is running under Windows. -* proxy_cache_time - Time after which the cache is cleared. Default value is one day. -* pushpoll_frequency - -* qsearch_limit - Default value is 100. -* relay_server - Experimental Diaspora feature. Address of the relay server where public posts should be send to. For example https://podrelay.net -* relay_subscribe (Boolean) - Enables the receiving of public posts from the relay. They will be included in the search and on the community page when it is set up to show all public items. -* relay_scope - Can be "all" or "tags". "all" means that every public post should be received. "tags" means that only posts witt selected tags should be received. -* relay_server_tags - Comma separated list of tags for the "tags" subscription (see "relay_scrope") -* relay_user_tags (Boolean) - If enabled, the tags from the saved searches will used for the "tags" subscription in addition to the "relay_server_tags". -* remove_multiplicated_lines (Boolean) - If enabled, multiple linefeeds in items are stripped to a single one. -* show_unsupported_addons (Boolean) - Show all addons including the unsupported ones. -* show_unsupported_themes (Boolean) - Show all themes including the unsupported ones. -* throttle_limit_day - Maximum number of posts that a user can send per day with the API. -* throttle_limit_week - Maximum number of posts that a user can send per week with the API. -* throttle_limit_month - Maximum number of posts that a user can send per month with the API. -* wall-to-wall_share (Boolean) - Displays forwarded posts like "wall-to-wall" posts. -* worker (Boolean) - (Experimental) Use the worker system instead of calling several background processes. Reduces the overall load and speeds up item delivery. -* worker_dont_fork (Boolean) - if enabled, the workers are only called from the poller process. Useful on systems that permit the use of "proc_open". -* worker_queues - Number of parallel workers. Default value is 10 queues. -* xrd_timeout - Timeout for fetching the XRD links. Default value is 20 seconds. +* **allowed_link_protocols** (Array) - Allowed protocols in links URLs, add at your own risk. http is always allowed. +* **birthday_input_format** - Default value is "ymd". +* **block_local_dir** (Boolean) - Blocks the access to the directory of the local users. +* **curl_range_bytes** - Maximum number of bytes that should be fetched. Default is 0, which mean "no limit". +* **db_log** - Name of a logfile to log slow database queries +* **db_loglimit** - If a database call lasts longer than this value it is logged +* **db_log_index** - Name of a logfile to log queries with bad indexes +* **db_log_index_watch** - Watchlist of indexes to watch +* **db_loglimit_index** - Number of index rows needed to be logged for indexes on the watchlist +* **db_loglimit_index_high** - Number of index rows to be logged anyway (for any index) +* **db_log_index_blacklist** - Blacklist of indexes that shouldn't be watched +* **dbclean** (Boolean) - Enable the automatic database cleanup process +* **default_service_class** - +* **delivery_batch_count** - Number of deliveries per process. Default value is 1. (Disabled when using the worker) +* **diaspora_test** (Boolean) - For development only. Disables the message transfer. +* **directory** - The path to global directory. If not set then "http://dir.friendi.ca" is used. +* **disable_email_validation** (Boolean) - Disables the check if a mail address is in a valid format and can be resolved via DNS. +* **disable_url_validation** (Boolean) - Disables the DNS lookup of an URL. +* **event_input_format** - Default value is "ymd". +* **frontend_worker_timeout** - Value in minutes after we think that a frontend task was killed by the webserver. Default value is 10. +* **ignore_cache** (Boolean) - For development only. Disables the item cache. +* **like_no_comment** (Boolean) - Don't update the "commented" value of an item when it is liked. +* **local_block** (Boolean) - Used in conjunction with "block_public". +* **local_search** (Boolean) - Blocks the search for not logged in users to prevent crawlers from blocking your system. +* **max_connections** - The poller process isn't started when the maximum level of the possible database connections are used. When the system can't detect the maximum numbers of connection then this value can be used. +* **max_connections_level** - The maximum level of connections that are allowed to let the poller start. It is a percentage value. Default value is 75. +* **max_contact_queue** - Default value is 500. +* **max_batch_queue** - Default value is 1000. +* **max_processes_backend** - Maximum number of concurrent database processes for background tasks. Default value is 5. +* **max_processes_frontend** - Maximum number of concurrent database processes for foreground tasks. Default value is 20. +* **memcache** (Boolean) - Use memcache. To use memcache the PECL extension "memcache" has to be installed and activated. +* **memcache_host** - Hostname of the memcache daemon. Default is '127.0.0.1'. +* **memcache_port** - Portnumber of the memcache daemon. Default is 11211. +* **no_count** (Boolean) - Don't do count calculations (currently only when showing albums) +* **no_oembed** (Boolean) - Don't use OEmbed to fetch more information about a link. +* **no_oembed_rich_content** (Boolean) - Don't show the rich content (e.g. embedded PDF). +* **no_smilies** (Boolean) - Don't show smilies. +* **no_view_full_size** (Boolean) - Don't add the link "View full size" under a resized image. +* **optimize_items** (Boolean) - Triggers an SQL command to optimize the item table before expiring items. +* **ostatus_poll_timeframe** - Defines how old an item can be to try to complete the conversation with it. +* **paranoia** (Boolean) - Log out users if their IP address changed. +* **permit_crawling** (Boolean) - Restricts the search for not logged in users to one search per minute. +* **profiler** (Boolean) - Enable internal timings to help optimize code. Needed for "rendertime" addon. Default is false. +* **free_crawls** - Number of "free" searches when "permit_crawling" is activated (Default value is 10) +* **crawl_permit_period** - Period in seconds between allowed searches when the number of free searches is reached and "permit_crawling" is activated (Default value is 60) +* **png_quality** - Default value is 8. +* **proc_windows** (Boolean) - Should be enabled if Friendica is running under Windows. +* **proxy_cache_time** - Time after which the cache is cleared. Default value is one day. +* **pushpoll_frequency** - +* **qsearch_limit** - Default value is 100. +* **relay_server** - Experimental Diaspora feature. Address of the relay server where public posts should be send to. For example https://podrelay.net +* **relay_subscribe** (Boolean) - Enables the receiving of public posts from the relay. They will be included in the search and on the community page when it is set up to show all public items. +* **relay_scope** - Can be "all" or "tags". "all" means that every public post should be received. "tags" means that only posts with selected tags should be received. +* **relay_server_tags** - Comma separated list of tags for the "tags" subscription (see "relay_scrope") +* **relay_user_tags** (Boolean) - If enabled, the tags from the saved searches will used for the "tags" subscription in addition to the "relay_server_tags". +* **remove_multiplicated_lines** (Boolean) - If enabled, multiple linefeeds in items are stripped to a single one. +* **show_unsupported_addons** (Boolean) - Show all addons including the unsupported ones. +* **show_unsupported_themes** (Boolean) - Show all themes including the unsupported ones. +* **throttle_limit_day** - Maximum number of posts that a user can send per day with the API. +* **throttle_limit_week** - Maximum number of posts that a user can send per week with the API. +* **throttle_limit_month** - Maximum number of posts that a user can send per month with the API. +* **wall-to-wall_share** (Boolean) - Displays forwarded posts like "wall-to-wall" posts. +* **worker_cooldown** - Cooldown time after each worker function call. Default value is 0 seconds. +* **xrd_timeout** - Timeout for fetching the XRD links. Default value is 20 seconds. ## service_class ## -* upgrade_link - +* **upgrade_link** - ## experimentals ## -* exp_themes (Boolean) - Show experimental themes as well. +* **exp_themes** (Boolean) - Show experimental themes as well. ## theme ## -* hide_eventlist (Boolean) - Don't show the birthdays and events on the profile and network page +* **hide_eventlist** (Boolean) - Don't show the birthdays and events on the profile and network page # Administrator Options # -Enabling the admin panel for an account, and thus making the account holder -admin of the node, is done by setting the variable +Enabling the admin panel for an account, and thus making the account holder admin of the node, is done by setting the variable $a->config['admin_email'] = "someone@example.com"; -where you have to match the email address used for the account with the one you -enter to the .htconfig file. If more then one account should be able to access -the admin panel, seperate the email addresses with a comma. +Where you have to match the email address used for the account with the one you enter to the .htconfig file. +If more then one account should be able to access the admin panel, seperate the email addresses with a comma. $a->config['admin_email'] = "someone@example.com,someonelese@example.com"; -If you want to have a more personalized closing line for the notification -emails you can set a variable for the admin_name. +If you want to have a more personalized closing line for the notification emails you can set a variable for the admin_name. $a->config['admin_name'] = "Marvin"; - diff --git a/doc/img/editor_frio.png b/doc/img/editor_frio.png new file mode 100644 index 0000000000..8428b34382 Binary files /dev/null and b/doc/img/editor_frio.png differ diff --git a/doc/readme.md b/doc/readme.md index 068d0c9c5c..98b637a22e 100644 --- a/doc/readme.md +++ b/doc/readme.md @@ -27,7 +27,7 @@ Friendica Documentation and Resources **Technical Documentation** * [Install](help/Install) -* [Settings](help/Settings) +* [Settings & Admin Panel](help/Settings) * [Plugins](help/Plugins) * [Installing Connectors (Twitter/GNU Social)](help/Installing-Connectors) * [Install an ejabberd server (XMPP chat) with synchronized credentials](help/install-ejabberd) diff --git a/doc/snarty3-templates.md b/doc/smarty3-templates.md similarity index 100% rename from doc/snarty3-templates.md rename to doc/smarty3-templates.md diff --git a/doc/themes.md b/doc/themes.md index add44c776b..b553debfd7 100644 --- a/doc/themes.md +++ b/doc/themes.md @@ -122,10 +122,11 @@ the 1st part of the line is the name of the CSS file (without the .css) the 2nd Calling the t() function with the common name makes the string translateable. The selected 1st part will be saved in the database by the theme_post function. - function theme_post(&$a){ + function theme_post(App $a){ // non local users shall not pass - if(! local_user()) + if (! local_user()) { return; + } // if the one specific submit button was pressed then proceed if (isset($_POST['duepuntozero-settings-submit'])){ // and save the selection key into the personal config of the user @@ -167,7 +168,7 @@ The content of this file should be something like theme_info = array( 'extends' => 'duepuntozero'. ); @@ -250,7 +251,7 @@ Next crucial part of the theme.php file is a definition of an init function. The name of the function is _init. So in the case of quattro it is - function quattro_init(&$a) { + function quattro_init(App $a) { $a->theme_info = array(); set_template_engine($a, 'smarty3'); } diff --git a/doc/translations.md b/doc/translations.md index b874e9ea2e..61d91bee5b 100644 --- a/doc/translations.md +++ b/doc/translations.md @@ -1,45 +1,37 @@ Friendica translations ====================== +* [Home](help) + Translation Process ------------------- -The strings used in the UI of Friendica is translated at [Transifex] [1] and then -included in the git repository at github. If you want to help with translation -for any language, be it correcting terms or translating friendica to a -currently not supported language, please register an account at transifex.com -and contact the friendica translation team there. +The strings used in the UI of Friendica is translated at [Transifex] [1] and then included in the git repository at github. +If you want to help with translation for any language, be it correcting terms or translating friendica to a currently not supported language, please register an account at transifex.com and contact the friendica translation team there. -Translating friendica is simple. Just use the online tool at transifex. If you -don't want to deal with git & co. that is fine, we check the status of the -translations regularly and import them into the source tree at github so that -others can use them. +Translating friendica is simple. +Just use the online tool at transifex. +If you don't want to deal with git & co. that is fine, we check the status of the translations regularly and import them into the source tree at github so that others can use them. -We do not include every translation from transifex in the source tree to avoid -a scattered and disturbed overall experience. As an uneducated guess we have a -lower limit of 50% translated strings before we include the language (for the -core message.po file, addont translation will be included once all strings of -an addon are translated. This limit is judging only by the amount of translated -strings under the assumption that the most prominent strings for the UI will be -translated first by a translation team. If you feel your translation useable -before this limit, please contact us and we will probably include your teams -work in the source tree. +We do not include every translation from transifex in the source tree to avoid a scattered and disturbed overall experience. +As an uneducated guess we have a lower limit of 50% translated strings before we include the language (for the core messages.po file, addont translation will be included once all strings of an addon are translated. +This limit is judging only by the amount of translated strings under the assumption that the most prominent strings for the UI will be translated first by a translation team. +If you feel your translation useable before this limit, please contact us and we will probably include your teams work in the source tree. -If you want to get your work into the source tree yourself, feel free to do so -and contact us with and question that arises. The process is simple and -friendica ships with all the tools necessary. +If you want to help translating, please concentrate on the core messages.po file first. +We will only include translations with a sufficient translated messages.po file. +Translations of addons will only be included, when the core file is included as well. + +If you want to get your work into the source tree yourself, feel free to do so and contact us with and question that arises. +The process is simple and friendica ships with all the tools necessary. The location of the translated files in the source tree is - /view/LNG-CODE/ + /view/lang/LNG-CODE/ where LNG-CODE is the language code used, e.g. de for German or fr for French. -For the email templates (the *.tpl files) just place them into the directory -and you are done. The translated strings come as a "message.po" file from -transifex which needs to be translated into the PHP file friendica uses. To do -so, place the file in the directory mentioned above and use the "po2php" -utility from the util directory of your friendica installation. +The translated strings come as a "message.po" file from transifex which needs to be translated into the PHP file friendica uses. +To do so, place the file in the directory mentioned above and use the "po2php" utility from the util directory of your friendica installation. -Assuming you want to convert the German localization which is placed in -view/de/message.po you would do the following. +Assuming you want to convert the German localization which is placed in view/lang/de/message.po you would do the following. 1. Navigate at the command prompt to the base directory of your friendica installation @@ -47,20 +39,20 @@ view/de/message.po you would do the following. 2. Execute the po2php script, which will place the translation in the strings.php file that is used by friendica. - $> php util/po2php.php view/de/message.po + $> php util/po2php.php view/lang/de/messages.po + + The output of the script will be placed at view/lang/de/strings.php where + friendica is expecting it, so you can test your translation immediately. - The output of the script will be placed at view/de/strings.php where - froemdoca os expecting it, so you can test your translation mmediately. - 3. Visit your friendica page to check if it still works in the language you just translated. If not try to find the error, most likely PHP will give you a hint in the log/warnings.about the error. - + For debugging you can also try to "run" the file with PHP. This should not give any output if the file is ok but might give a hint for searching the bug in the file. - $> php view/de/strings.php + $> php view/lang/de/strings.php 4. commit the two files with a meaningful commit message to your git repository, push it to your fork of the friendica repository at github and @@ -69,27 +61,40 @@ view/de/message.po you would do the following. Utilities --------- -Additional to the po2php script there are some more utilities for translation -in the "util" directory of the friendica source tree. If you only want to -translate friendica into another language you wont need any of these tools most -likely but it gives you an idea how the translation process of friendica -works. +Additional to the po2php script there are some more utilities for translation in the "util" directory of the friendica source tree. +If you only want to translate friendica into another language you wont need any of these tools most likely but it gives you an idea how the translation process of friendica works. For further information see the utils/README file. -Known Problems --------------- +Transifex-Client +---------------- -Friendica uses the language setting of the visitors browser to determain the -language for the UI. Most of the time this works, but there are some known -quirks. +Transifex has a client program which let you interact with the translation files in a similar way to git. +Help for the client can be found at the [Transifex Help Center] [2]. +Here we will only cover basic usage. -One is that some browsers, like Safari, do the setting to "de-de" but friendica -only has a "de" localisation. A workaround would be to add a symbolic link -from - $friendica/view/de-de -pointing to - $friendica/view/de +After installation of the client, you should have a `tx` command available on your system. +To use it, first create a configuration file with your credentials. +On Linux this file should be placed into your home directory `~/.transifexrc`. +The content of the file should be something like the following: + + [https://www.transifex.com] + username = user + token = + password = p@ssw0rd + hostname = https://www.transifex.com + +Since Friendica version 3.5.1 we ship configuration files for the Transifex client in the core repository and the addon repository. +To update the translation files after you have translated strings of e.g. Esperanto in the web-UI of transifex you can use `tx` to download the file. + + $> tx pull -l eo + +And then use the `po2php` utility described above to convert the `messages.po` file to the `strings.php` file Friendica is loading. + + $> php util/po2php.php view/lang/eo/messages.po + +Afterwards, just commit the two changed files to a feature branch of your Friendica repository, push the changes to github and open a pull request for your changes. [1]: https://www.transifex.com/projects/p/friendica/ +[2]: https://docs.transifex.com/client/introduction diff --git a/doc/upgrade.md b/doc/upgrade.md new file mode 100644 index 0000000000..778f9355e6 --- /dev/null +++ b/doc/upgrade.md @@ -0,0 +1,34 @@ +# Considerations before upgrading Friendica + +* [Home](help) + +## MySQL >= 5.7.4 + +Starting from MySQL version 5.7.4, the IGNORE keyword in ALTER TABLE statements is ignored. +This prevents automatic table deduplication if a UNIQUE index is added to a Friendica table's structure. +If a DB update fails for you while creating a UNIQUE index, make sure to manually deduplicate the table before trying the update again. + +### Manual deduplication + +There are two main ways of doing it, either by manually removing the duplicates or by recreating the table. +Manually removing the duplicates is usually faster if they're not too numerous. +To manually remove the duplicates, you need to know the UNIQUE index columns available in `database.sql`. + +```SQL +SELECT GROUP_CONCAT(id), , count(*) as count FROM users +GROUP BY HAVING count >= 2; + +/* delete or merge duplicate from above query */; +``` + +If there are too many rows to handle manually, you can create a new table with the same structure as the table with duplicates and insert the existing content with INSERT IGNORE. +To recreate the table you need to know the table structure available in `database.sql`. + +```SQL +CREATE TABLE _new ; +INSERT IGNORE INTO _new SELECT * FROM ; +DROP TABLE ; +RENAME TABLE _new TO ; +``` + +This method is slower overall, but it is better suited for large numbers of duplicates. \ No newline at end of file diff --git a/friendica_test_data.sql b/friendica_test_data.sql index c39a057651..45080e44d0 100644 --- a/friendica_test_data.sql +++ b/friendica_test_data.sql @@ -1657,35 +1657,6 @@ LOCK TABLES `tokens` WRITE; /*!40000 ALTER TABLE `tokens` ENABLE KEYS */; UNLOCK TABLES; --- --- Table structure for table `unique_contacts` --- - -DROP TABLE IF EXISTS `unique_contacts`; -/*!40101 SET @saved_cs_client = @@character_set_client */; -/*!40101 SET character_set_client = utf8 */; -CREATE TABLE `unique_contacts` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `url` varchar(255) NOT NULL DEFAULT '', - `nick` varchar(255) NOT NULL DEFAULT '', - `name` varchar(255) NOT NULL DEFAULT '', - `avatar` varchar(255) NOT NULL DEFAULT '', - `location` varchar(255) NOT NULL DEFAULT '', - `about` text NOT NULL, - PRIMARY KEY (`id`), - KEY `url` (`url`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; -/*!40101 SET character_set_client = @saved_cs_client */; - --- --- Dumping data for table `unique_contacts` --- - -LOCK TABLES `unique_contacts` WRITE; -/*!40000 ALTER TABLE `unique_contacts` DISABLE KEYS */; -/*!40000 ALTER TABLE `unique_contacts` ENABLE KEYS */; -UNLOCK TABLES; - -- -- Table structure for table `user` -- diff --git a/htconfig.php b/htconfig.php index 508de9a323..41317c4608 100644 --- a/htconfig.php +++ b/htconfig.php @@ -16,11 +16,20 @@ $db_user = 'mysqlusername'; $db_pass = 'mysqlpassword'; $db_data = 'mysqldatabasename'; +// Set the database connection charset to UTF8. +// Changing this value will likely corrupt the special characters. +// You have been warned. +$a->config['system']['db_charset'] = "utf8mb4"; + // Choose a legal default timezone. If you are unsure, use "America/Los_Angeles". // It can be changed later and only applies to timestamps for anonymous viewers. $default_timezone = 'America/Los_Angeles'; +// Default system language + +$a->config['system']['language'] = 'en'; + // What is your site name? $a->config['sitename'] = "Friendica Social Network"; @@ -55,13 +64,20 @@ $a->config['php_path'] = 'php'; $a->config['system']['huburl'] = '[internal]'; +// Server-to-server private message encryption (RINO) is allowed by default. +// Encryption will only be provided if this setting is set to a non zero +// value and the PHP mcrypt extension is installed on both systems +// set to 0 to disable, 2 to enable, 1 is deprecated but wont need mcrypt + +$a->config['system']['rino_encrypt'] = 2; + // allowed themes (change this from admin panel after installation) -$a->config['system']['allowed_themes'] = 'dispy,quattro,vier,darkzero,duepuntozero,greenzero,purplezero,slackr,diabook'; +$a->config['system']['allowed_themes'] = 'quattro,vier,duepuntozero,smoothly'; // default system theme -$a->config['system']['theme'] = 'duepuntozero'; +$a->config['system']['theme'] = 'vier'; // By default allow pseudonyms @@ -73,3 +89,6 @@ $a->config['system']['no_regfullname'] = true; // Location of the global directory $a->config['system']['directory'] = 'http://dir.friendi.ca'; + +// Allowed protocols in link URLs; HTTP protocols always are accepted +$a->config['system']['allowed_link_protocols'] = array('ftp', 'ftps', 'mailto', 'cid', 'gopher'); diff --git a/include/Contact.php b/include/Contact.php index 340b3ec6fa..2aab828f8a 100644 --- a/include/Contact.php +++ b/include/Contact.php @@ -1,5 +1,6 @@ get_baseurl()); + goaway(App::get_baseurl()); } } function contact_remove($id) { - $r = q("select uid from contact where id = %d limit 1", + // We want just to make sure that we don't delete our "self" contact + $r = q("SELECT `uid` FROM `contact` WHERE `id` = %d AND NOT `self` LIMIT 1", intval($id) ); - if((! count($r)) || (! intval($r[0]['uid']))) + if (!dbm::is_result($r) || !intval($r[0]['uid'])) { return; + } $archive = get_pconfig($r[0]['uid'], 'system','archive_removed_contacts'); - if($archive) { + if ($archive) { q("update contact set `archive` = 1, `network` = 'none', `writable` = 0 where id = %d", intval($id) ); return; } - q("DELETE FROM `contact` WHERE `id` = %d", - intval($id) - ); - q("DELETE FROM `item` WHERE `contact-id` = %d ", - intval($id) - ); - q("DELETE FROM `photo` WHERE `contact-id` = %d ", - intval($id) - ); - q("DELETE FROM `mail` WHERE `contact-id` = %d ", - intval($id) - ); - q("DELETE FROM `event` WHERE `cid` = %d ", - intval($id) - ); - q("DELETE FROM `queue` WHERE `cid` = %d ", - intval($id) - ); + q("DELETE FROM `contact` WHERE `id` = %d", intval($id)); + // Delete the rest in the background + proc_run(PRIORITY_LOW, 'include/remove_contact.php', $id); } @@ -100,40 +88,31 @@ function contact_remove($id) { function terminate_friendship($user,$self,$contact) { - + /// @TODO Get rid of this, include/datetime.php should care about it by itself $a = get_app(); require_once('include/datetime.php'); - if($contact['network'] === NETWORK_OSTATUS) { + if ($contact['network'] === NETWORK_OSTATUS) { - $slap = replace_macros(get_markup_template('follow_slap.tpl'), array( - '$name' => $user['username'], - '$profile_page' => $a->get_baseurl() . '/profile/' . $user['nickname'], - '$photo' => $self['photo'], - '$thumb' => $self['thumb'], - '$published' => datetime_convert('UTC','UTC', 'now', ATOM_TIME), - '$item_id' => 'urn:X-dfrn:' . $a->get_hostname() . ':unfollow:' . get_guid(32), - '$title' => '', - '$type' => 'text', - '$content' => t('stopped following'), - '$nick' => $user['nickname'], - '$verb' => 'http://ostatus.org/schema/1.0/unfollow', // ACTIVITY_UNFOLLOW, - '$ostat_follow' => '' // 'http://ostatus.org/schema/1.0/unfollow' . "\r\n" - )); + require_once('include/ostatus.php'); - if((x($contact,'notify')) && (strlen($contact['notify']))) { + // create an unfollow slap + $item = array(); + $item['verb'] = NAMESPACE_OSTATUS."/unfollow"; + $item['follow'] = $contact["url"]; + $slap = ostatus::salmon($item, $user); + + if ((x($contact,'notify')) && (strlen($contact['notify']))) { require_once('include/salmon.php'); slapper($user,$contact['notify'],$slap); } - } - elseif($contact['network'] === NETWORK_DIASPORA) { + } elseif ($contact['network'] === NETWORK_DIASPORA) { require_once('include/diaspora.php'); - diaspora_unshare($user,$contact); - } - elseif($contact['network'] === NETWORK_DFRN) { - require_once('include/items.php'); - dfrn_deliver($user,$contact,'placeholder', 1); + Diaspora::send_unshare($user,$contact); + } elseif ($contact['network'] === NETWORK_DFRN) { + require_once('include/dfrn.php'); + dfrn::deliver($user,$contact,'placeholder', 1); } } @@ -145,7 +124,6 @@ function terminate_friendship($user,$self,$contact) { // This provides for the possibility that their database is temporarily messed // up or some other transient event and that there's a possibility we could recover from it. -if(! function_exists('mark_for_death')) { function mark_for_death($contact) { if($contact['archive']) @@ -156,12 +134,23 @@ function mark_for_death($contact) { dbesc(datetime_convert()), intval($contact['id']) ); - } - else { - // TODO: We really should send a notification to the owner after 2-3 weeks - // so they won't be surprised when the contact vanishes and can take - // remedial action if this was a serious mistake or glitch + if ($contact['url'] != '') { + q("UPDATE `contact` SET `term-date` = '%s' + WHERE `nurl` = '%s' AND `term-date` <= '1000-00-00'", + dbesc(datetime_convert()), + dbesc(normalise_link($contact['url'])) + ); + } + } else { + + /// @todo + /// We really should send a notification to the owner after 2-3 weeks + /// so they won't be surprised when the contact vanishes and can take + /// remedial action if this was a serious mistake or glitch + + /// @todo + /// Check for contact vitality via probing $expiry = $contact['term-date'] . ' + 32 days '; if(datetime_convert() > datetime_convert('UTC','UTC',$expiry)) { @@ -170,187 +159,294 @@ function mark_for_death($contact) { // archive them rather than delete // though if the owner tries to unarchive them we'll start the whole process over again - q("update contact set `archive` = 1 where id = %d", + q("UPDATE `contact` SET `archive` = 1 WHERE `id` = %d", intval($contact['id']) ); - q("UPDATE `item` SET `private` = 2 WHERE `contact-id` = %d AND `uid` = %d", intval($contact['id']), intval($contact['uid'])); - - //contact_remove($contact['id']); + if ($contact['url'] != '') { + q("UPDATE `contact` SET `archive` = 1 WHERE `nurl` = '%s'", + dbesc(normalise_link($contact['url'])) + ); + } } } -}} +} -if(! function_exists('unmark_for_death')) { function unmark_for_death($contact) { + + $r = q("SELECT `term-date` FROM `contact` WHERE `id` = %d AND `term-date` > '%s'", + intval($contact['id']), + dbesc('1000-00-00 00:00:00') + ); + + // We don't need to update, we never marked this contact as dead + if (!dbm::is_result($r)) { + return; + } + // It's a miracle. Our dead contact has inexplicably come back to life. q("UPDATE `contact` SET `term-date` = '%s' WHERE `id` = %d", dbesc('0000-00-00 00:00:00'), intval($contact['id']) ); -}} -function get_contact_details_by_url($url, $uid = -1) { - if ($uid == -1) + if ($contact['url'] != '') { + q("UPDATE `contact` SET `term-date` = '%s' WHERE `nurl` = '%s'", + dbesc('0000-00-00 00:00:00'), + dbesc(normalise_link($contact['url'])) + ); + } +} + +/** + * @brief Get contact data for a given profile link + * + * The function looks at several places (contact table and gcontact table) for the contact + * It caches its result for the same script execution to prevent duplicate calls + * + * @param string $url The profile link + * @param int $uid User id + * @param array $default If not data was found take this data as default value + * + * @return array Contact data + */ +function get_contact_details_by_url($url, $uid = -1, $default = array()) { + static $cache = array(); + + if ($uid == -1) { $uid = local_user(); - - $r = q("SELECT `id` AS `gid`, `url`, `name`, `nick`, `addr`, `photo`, `location`, `about`, `keywords`, `gender`, `community`, `network` FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1", - dbesc(normalise_link($url))); - - if ($r) { - $profile = $r[0]; - - if ((($profile["addr"] == "") OR ($profile["name"] == "")) AND - in_array($profile["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))) - proc_run('php',"include/update_gcontact.php", $profile["gid"]); - - } else { - $r = q("SELECT `url`, `name`, `nick`, `avatar` AS `photo`, `location`, `about` FROM `unique_contacts` WHERE `url` = '%s'", - dbesc(normalise_link($url))); - - if (count($r)) { - $profile = $r[0]; - $profile["keywords"] = ""; - $profile["gender"] = ""; - $profile["community"] = false; - $profile["network"] = ""; - $profile["addr"] = ""; - } } - // Fetching further contact data from the contact table - $r = q("SELECT `id`, `uid`, `url`, `network`, `name`, `nick`, `addr`, `location`, `about`, `keywords`, `gender`, `photo`, `addr`, `forum`, `prv`, `bd` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d AND `network` = '%s'", - dbesc(normalise_link($url)), intval($uid), dbesc($profile["network"])); + if (isset($cache[$url][$uid])) { + return $cache[$url][$uid]; + } - if (!count($r)) - $r = q("SELECT `id`, `uid`, `url`, `network`, `name`, `nick`, `addr`, `location`, `about`, `keywords`, `gender`, `photo`, `addr`, `forum`, `prv`, `bd` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d", + // Fetch contact data from the contact table for the given user + $r = q("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`, + `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self` + FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d", dbesc(normalise_link($url)), intval($uid)); - if (!count($r)) - $r = q("SELECT `id`, `uid`, `url`, `network`, `name`, `nick`, `addr`, `location`, `about`, `keywords`, `gender`, `photo`, `addr`, `forum`, `prv`, `bd` FROM `contact` WHERE `nurl` = '%s' AND `uid` = 0", - dbesc(normalise_link($url))); + // Fetch the data from the contact table with "uid=0" (which is filled automatically) + if (!dbm::is_result($r)) + $r = q("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`, + `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self` + FROM `contact` WHERE `nurl` = '%s' AND `uid` = 0", + dbesc(normalise_link($url))); - if ($r) { - if (isset($r[0]["url"]) AND $r[0]["url"]) - $profile["url"] = $r[0]["url"]; - if (isset($r[0]["name"]) AND $r[0]["name"]) - $profile["name"] = $r[0]["name"]; - if (isset($r[0]["nick"]) AND $r[0]["nick"] AND ($profile["nick"] == "")) - $profile["nick"] = $r[0]["nick"]; - if (isset($r[0]["addr"]) AND $r[0]["addr"] AND ($profile["addr"] == "")) - $profile["addr"] = $r[0]["addr"]; - if (isset($r[0]["photo"]) AND $r[0]["photo"]) - $profile["photo"] = $r[0]["photo"]; - if (isset($r[0]["location"]) AND $r[0]["location"]) - $profile["location"] = $r[0]["location"]; - if (isset($r[0]["about"]) AND $r[0]["about"]) - $profile["about"] = $r[0]["about"]; - if (isset($r[0]["keywords"]) AND $r[0]["keywords"]) - $profile["keywords"] = $r[0]["keywords"]; - if (isset($r[0]["gender"]) AND $r[0]["gender"]) - $profile["gender"] = $r[0]["gender"]; - if (isset($r[0]["forum"]) OR isset($r[0]["prv"])) - $profile["community"] = ($r[0]["forum"] OR $r[0]["prv"]); - if (isset($r[0]["network"]) AND $r[0]["network"]) - $profile["network"] = $r[0]["network"]; - if (isset($r[0]["addr"]) AND $r[0]["addr"]) - $profile["addr"] = $r[0]["addr"]; - if (isset($r[0]["bd"]) AND $r[0]["bd"]) - $profile["bd"] = $r[0]["bd"]; - if ($r[0]["uid"] == 0) - $profile["cid"] = 0; - else - $profile["cid"] = $r[0]["id"]; - } else - $profile["cid"] = 0; + // Fetch the data from the gcontact table + if (!dbm::is_result($r)) + $r = q("SELECT 0 AS `id`, 0 AS `cid`, `id` AS `gid`, 0 AS `zid`, 0 AS `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, '' AS `xmpp`, + `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self` + FROM `gcontact` WHERE `nurl` = '%s'", + dbesc(normalise_link($url))); + if (dbm::is_result($r)) { + // If there is more than one entry we filter out the connector networks + if (count($r) > 1) { + foreach ($r AS $id => $result) { + if ($result["network"] == NETWORK_STATUSNET) { + unset($r[$id]); + } + } + } + + $profile = array_shift($r); + + // "bd" always contains the upcoming birthday of a contact. + // "birthday" might contain the birthday including the year of birth. + if ($profile["birthday"] != "0000-00-00") { + $bd_timestamp = strtotime($profile["birthday"]); + $month = date("m", $bd_timestamp); + $day = date("d", $bd_timestamp); + + $current_timestamp = time(); + $current_year = date("Y", $current_timestamp); + $current_month = date("m", $current_timestamp); + $current_day = date("d", $current_timestamp); + + $profile["bd"] = $current_year."-".$month."-".$day; + $current = $current_year."-".$current_month."-".$current_day; + + if ($profile["bd"] < $current) { + $profile["bd"] = (++$current_year)."-".$month."-".$day; + } + } else { + $profile["bd"] = "0000-00-00"; + } + } else { + $profile = $default; + } + + if (($profile["photo"] == "") AND isset($default["photo"])) { + $profile["photo"] = $default["photo"]; + } + + if (($profile["name"] == "") AND isset($default["name"])) { + $profile["name"] = $default["name"]; + } + + if (($profile["network"] == "") AND isset($default["network"])) { + $profile["network"] = $default["network"]; + } + + if (($profile["thumb"] == "") AND isset($profile["photo"])) { + $profile["thumb"] = $profile["photo"]; + } + + if (($profile["micro"] == "") AND isset($profile["thumb"])) { + $profile["micro"] = $profile["thumb"]; + } + + if ((($profile["addr"] == "") OR ($profile["name"] == "")) AND ($profile["gid"] != 0) AND + in_array($profile["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))) { + proc_run(PRIORITY_LOW, "include/update_gcontact.php", $profile["gid"]); + } + + // Show contact details of Diaspora contacts only if connected if (($profile["cid"] == 0) AND ($profile["network"] == NETWORK_DIASPORA)) { $profile["location"] = ""; $profile["about"] = ""; + $profile["gender"] = ""; + $profile["birthday"] = "0000-00-00"; } - return($profile); + $cache[$url][$uid] = $profile; + + return $profile; } -if(! function_exists('contact_photo_menu')){ -function contact_photo_menu($contact, $uid = 0) { +/** + * @brief Get contact data for a given address + * + * The function looks at several places (contact table and gcontact table) for the contact + * + * @param string $addr The profile link + * @param int $uid User id + * + * @return array Contact data + */ +function get_contact_details_by_addr($addr, $uid = -1) { + static $cache = array(); + if ($uid == -1) { + $uid = local_user(); + } + + // Fetch contact data from the contact table for the given user + $r = q("SELECT `id`, `id` AS `cid`, 0 AS `gid`, 0 AS `zid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`, + `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, `self` + FROM `contact` WHERE `addr` = '%s' AND `uid` = %d", + dbesc($addr), intval($uid)); + + // Fetch the data from the contact table with "uid=0" (which is filled automatically) + if (!dbm::is_result($r)) + $r = q("SELECT `id`, 0 AS `cid`, `id` AS `zid`, 0 AS `gid`, `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, `xmpp`, + `keywords`, `gender`, `photo`, `thumb`, `micro`, `forum`, `prv`, (`forum` | `prv`) AS `community`, `contact-type`, `bd` AS `birthday`, 0 AS `self` + FROM `contact` WHERE `addr` = '%s' AND `uid` = 0", + dbesc($addr)); + + // Fetch the data from the gcontact table + if (!dbm::is_result($r)) + $r = q("SELECT 0 AS `id`, 0 AS `cid`, `id` AS `gid`, 0 AS `zid`, 0 AS `uid`, `url`, `nurl`, `alias`, `network`, `name`, `nick`, `addr`, `location`, `about`, '' AS `xmpp`, + `keywords`, `gender`, `photo`, `photo` AS `thumb`, `photo` AS `micro`, `community` AS `forum`, 0 AS `prv`, `community`, `contact-type`, `birthday`, 0 AS `self` + FROM `gcontact` WHERE `addr` = '%s'", + dbesc($addr)); + + if (!dbm::is_result($r)) { + $data = Probe::uri($addr); + + $profile = get_contact_details_by_url($data['url'], $uid); + } else { + $profile = $r[0]; + } + + return $profile; +} + +if (! function_exists('contact_photo_menu')) { +function contact_photo_menu($contact, $uid = 0) +{ $a = get_app(); - $contact_url=""; - $pm_url=""; - $status_link=""; - $photos_link=""; - $posts_link=""; - $contact_drop_link = ""; - $poke_link=""; + $contact_url = ''; + $pm_url = ''; + $status_link = ''; + $photos_link = ''; + $posts_link = ''; + $contact_drop_link = ''; + $poke_link = ''; - if ($uid == 0) + if ($uid == 0) { $uid = local_user(); + } - if ($contact["uid"] != $uid) { + if ($contact['uid'] != $uid) { if ($uid == 0) { $profile_link = zrl($contact['url']); - $menu = Array('profile' => array(t("View Profile"), $profile_link, true)); + $menu = Array('profile' => array(t('View Profile'), $profile_link, true)); return $menu; } $r = q("SELECT * FROM `contact` WHERE `nurl` = '%s' AND `network` = '%s' AND `uid` = %d", - dbesc($contact["nurl"]), dbesc($contact["network"]), intval($uid)); - if ($r) + dbesc($contact['nurl']), dbesc($contact['network']), intval($uid)); + if ($r) { return contact_photo_menu($r[0], $uid); - else { + } else { $profile_link = zrl($contact['url']); $connlnk = 'follow/?url='.$contact['url']; - $menu = Array( - 'profile' => array(t("View Profile"), $profile_link, true), - 'follow' => array(t("Connect/Follow"), $connlnk, true) - ); + $menu = array( + 'profile' => array(t('View Profile'), $profile_link, true), + 'follow' => array(t('Connect/Follow'), $connlnk, true) + ); return $menu; } } $sparkle = false; - if($contact['network'] === NETWORK_DFRN) { + if ($contact['network'] === NETWORK_DFRN) { $sparkle = true; - $profile_link = $a->get_baseurl() . '/redir/' . $contact['id']; - } - else + $profile_link = App::get_baseurl() . '/redir/' . $contact['id']; + } else { $profile_link = $contact['url']; - - if($profile_link === 'mailbox') - $profile_link = ''; - - if($sparkle) { - $status_link = $profile_link . "?url=status"; - $photos_link = $profile_link . "?url=photos"; - $profile_link = $profile_link . "?url=profile"; } - if (in_array($contact["network"], array(NETWORK_DFRN, NETWORK_DIASPORA))) - $pm_url = $a->get_baseurl() . '/message/new/' . $contact['id']; + if ($profile_link === 'mailbox') { + $profile_link = ''; + } - if ($contact["network"] == NETWORK_DFRN) - $poke_link = $a->get_baseurl() . '/poke/?f=&c=' . $contact['id']; + if ($sparkle) { + $status_link = $profile_link . '?url=status'; + $photos_link = $profile_link . '?url=photos'; + $profile_link = $profile_link . '?url=profile'; + } - $contact_url = $a->get_baseurl() . '/contacts/' . $contact['id']; - $posts_link = $a->get_baseurl() . "/contacts/" . $contact['id'] . '/posts'; - $contact_drop_link = $a->get_baseurl() . "/contacts/" . $contact['id'] . '/drop?confirm=1'; + if (in_array($contact['network'], array(NETWORK_DFRN, NETWORK_DIASPORA))) { + $pm_url = App::get_baseurl() . '/message/new/' . $contact['id']; + } + if ($contact['network'] == NETWORK_DFRN) { + $poke_link = App::get_baseurl() . '/poke/?f=&c=' . $contact['id']; + } + + $contact_url = App::get_baseurl() . '/contacts/' . $contact['id']; + + $posts_link = App::get_baseurl() . '/contacts/' . $contact['id'] . '/posts'; + $contact_drop_link = App::get_baseurl() . '/contacts/' . $contact['id'] . '/drop?confirm=1'; /** * menu array: * "name" => [ "Label", "link", (bool)Should the link opened in a new tab? ] */ - $menu = Array( + $menu = array( 'status' => array(t("View Status"), $status_link, true), 'profile' => array(t("View Profile"), $profile_link, true), - 'photos' => array(t("View Photos"), $photos_link,true), - 'network' => array(t("Network Posts"), $posts_link,false), - 'edit' => array(t("Edit Contact"), $contact_url, false), + 'photos' => array(t("View Photos"), $photos_link, true), + 'network' => array(t("Network Posts"), $posts_link, false), + 'edit' => array(t("View Contact"), $contact_url, false), 'drop' => array(t("Drop Contact"), $contact_drop_link, false), 'pm' => array(t("Send PM"), $pm_url, false), 'poke' => array(t("Poke"), $poke_link, false), @@ -363,9 +459,11 @@ function contact_photo_menu($contact, $uid = 0) { $menucondensed = array(); - foreach ($menu AS $menuname=>$menuitem) - if ($menuitem[1] != "") + foreach ($menu AS $menuname => $menuitem) { + if ($menuitem[1] != '') { $menucondensed[$menuname] = $menuitem; + } + } return $menucondensed; }} @@ -378,7 +476,7 @@ function random_profile() { ORDER BY rand() LIMIT 1", dbesc(NETWORK_DFRN)); - if(count($r)) + if (dbm::is_result($r)) return dirname($r[0]['url']); return ''; } @@ -407,13 +505,25 @@ function contacts_not_grouped($uid,$start = 0,$count = 0) { return $r; } -function get_contact($url, $uid = 0) { +/** + * @brief Fetch the contact id for a given url and user + * + * @param string $url Contact URL + * @param integer $uid The user id for the contact + * @param boolean $no_update Don't update the contact + * + * @return integer Contact ID + */ +function get_contact($url, $uid = 0, $no_update = false) { require_once("include/Scrape.php"); + logger("Get contact data for url ".$url." and user ".$uid." - ".App::callstack(), LOGGER_DEBUG);; + $data = array(); $contactid = 0; // is it an address in the format user@server.tld? + /// @todo use gcontact and/or the addr field for a lookup if (!strstr($url, "http") OR strstr($url, "@")) { $data = probe_url($url); $url = $data["url"]; @@ -421,12 +531,12 @@ function get_contact($url, $uid = 0) { return 0; } - $contact = q("SELECT `id`, `avatar-date` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d", + $contact = q("SELECT `id`, `avatar-date` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d ORDER BY `id` LIMIT 2", dbesc(normalise_link($url)), intval($uid)); if (!$contact) - $contact = q("SELECT `id`, `avatar-date` FROM `contact` WHERE `alias` IN ('%s', '%s') AND `uid` = %d", + $contact = q("SELECT `id`, `avatar-date` FROM `contact` WHERE `alias` IN ('%s', '%s') AND `uid` = %d ORDER BY `id` LIMIT 1", dbesc($url), dbesc(normalise_link($url)), intval($uid)); @@ -438,8 +548,9 @@ function get_contact($url, $uid = 0) { $update_photo = ($contact[0]['avatar-date'] < datetime_convert('','','now -7 days')); //$update_photo = ($contact[0]['avatar-date'] < datetime_convert('','','now -12 hours')); - if (!$update_photo) + if (!$update_photo OR $no_update) { return($contactid); + } } elseif ($uid != 0) return 0; @@ -447,19 +558,27 @@ function get_contact($url, $uid = 0) { $data = probe_url($url); // Does this address belongs to a valid network? - if (!in_array($data["network"], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA))) - return 0; + if (!in_array($data["network"], array(NETWORK_DFRN, NETWORK_OSTATUS, NETWORK_DIASPORA))) { + if ($uid != 0) + return 0; - // tempory programming. Can be deleted after 2015-02-07 - if (($data["alias"] == "") AND (normalise_link($data["url"]) != normalise_link($url))) - $data["alias"] = normalise_link($url); + // Get data from the gcontact table + $r = q("SELECT `name`, `nick`, `url`, `photo`, `addr`, `alias`, `network` FROM `gcontact` WHERE `nurl` = '%s'", + dbesc(normalise_link($url))); + if (!$r) + return 0; + + $data = $r[0]; + } + + $url = $data["url"]; if ($contactid == 0) { q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `addr`, `alias`, `notify`, `poll`, `name`, `nick`, `photo`, `network`, `pubkey`, `rel`, `priority`, - `batch`, `request`, `confirm`, `poco`, + `batch`, `request`, `confirm`, `poco`, `name-date`, `uri-date`, `writable`, `blocked`, `readonly`, `pending`) - VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', 1, 0, 0, 0)", + VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', 1, 0, 0, 0)", intval($uid), dbesc(datetime_convert()), dbesc($data["url"]), @@ -478,37 +597,246 @@ function get_contact($url, $uid = 0) { dbesc($data["batch"]), dbesc($data["request"]), dbesc($data["confirm"]), - dbesc($data["poco"]) + dbesc($data["poco"]), + dbesc(datetime_convert()), + dbesc(datetime_convert()) ); - $contact = q("SELECT `id` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d", + $contact = q("SELECT `id` FROM `contact` WHERE `nurl` = '%s' AND `uid` = %d ORDER BY `id` LIMIT 2", dbesc(normalise_link($data["url"])), intval($uid)); if (!$contact) return 0; $contactid = $contact[0]["id"]; + + // Update the newly created contact from data in the gcontact table + $r = q("SELECT `location`, `about`, `keywords`, `gender` FROM `gcontact` WHERE `nurl` = '%s'", + dbesc(normalise_link($data["url"]))); + if ($r) { + logger("Update contact ".$data["url"]); + q("UPDATE `contact` SET `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s' WHERE `id` = %d", + dbesc($r["location"]), dbesc($r["about"]), dbesc($r["keywords"]), + dbesc($r["gender"]), intval($contactid)); + } } + if ((count($contact) > 1) AND ($uid == 0) AND ($contactid != 0) AND ($url != "")) + q("DELETE FROM `contact` WHERE `nurl` = '%s' AND `id` != %d AND NOT `self`", + dbesc(normalise_link($url)), + intval($contactid)); + require_once("Photo.php"); - $photos = import_profile_photo($data["photo"],$uid,$contactid); + update_contact_avatar($data["photo"],$uid,$contactid); - q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `micro` = '%s', - `addr` = '%s', `alias` = '%s', `name` = '%s', `nick` = '%s', - `name-date` = '%s', `uri-date` = '%s', `avatar-date` = '%s' WHERE `id` = %d", - dbesc($photos[0]), - dbesc($photos[1]), - dbesc($photos[2]), - dbesc($data["addr"]), - dbesc($data["alias"]), - dbesc($data["name"]), - dbesc($data["nick"]), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - intval($contactid) - ); + $r = q("SELECT `addr`, `alias`, `name`, `nick` FROM `contact` WHERE `id` = %d", intval($contactid)); + + // This condition should always be true + if (!dbm::is_result($r)) + return $contactid; + + // Only update if there had something been changed + if (($data["addr"] != $r[0]["addr"]) OR + ($data["alias"] != $r[0]["alias"]) OR + ($data["name"] != $r[0]["name"]) OR + ($data["nick"] != $r[0]["nick"])) + q("UPDATE `contact` SET `addr` = '%s', `alias` = '%s', `name` = '%s', `nick` = '%s', + `name-date` = '%s', `uri-date` = '%s' WHERE `id` = %d", + dbesc($data["addr"]), + dbesc($data["alias"]), + dbesc($data["name"]), + dbesc($data["nick"]), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + intval($contactid) + ); return $contactid; } + +/** + * @brief Returns posts from a given gcontact + * + * @param App $a argv application class + * @param int $gcontact_id Global contact + * + * @return string posts in HTML + */ +function posts_from_gcontact(App $a, $gcontact_id) { + + require_once('include/conversation.php'); + + // There are no posts with "uid = 0" with connector networks + // This speeds up the query a lot + $r = q("SELECT `network` FROM `gcontact` WHERE `id` = %d", dbesc($gcontact_id)); + if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) + $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = %d AND `item`.`private`))"; + else + $sql = "`item`.`uid` = %d"; + + if(get_config('system', 'old_pager')) { + $r = q("SELECT COUNT(*) AS `total` FROM `item` + WHERE `gcontact-id` = %d and $sql", + intval($gcontact_id), + intval(local_user())); + + $a->set_pager_total($r[0]['total']); + } + + $r = q("SELECT `item`.`uri`, `item`.*, `item`.`id` AS `item_id`, + `author-name` AS `name`, `owner-avatar` AS `photo`, + `owner-link` AS `url`, `owner-avatar` AS `thumb` + FROM `item` + WHERE `gcontact-id` = %d AND $sql AND + NOT `deleted` AND NOT `moderated` AND `visible` + ORDER BY `item`.`created` DESC LIMIT %d, %d", + intval($gcontact_id), + intval(local_user()), + intval($a->pager['start']), + intval($a->pager['itemspage']) + ); + + $o = conversation($a,$r,'community',false); + + if(!get_config('system', 'old_pager')) { + $o .= alt_pager($a,count($r)); + } else { + $o .= paginate($a); + } + + return $o; +} +/** + * @brief Returns posts from a given contact url + * + * @param App $a argv application class + * @param string $contact_url Contact URL + * + * @return string posts in HTML + */ +function posts_from_contact_url(App $a, $contact_url) { + + require_once('include/conversation.php'); + + // There are no posts with "uid = 0" with connector networks + // This speeds up the query a lot + $r = q("SELECT `network`, `id` AS `author-id` FROM `contact` + WHERE `contact`.`nurl` = '%s' AND `contact`.`uid` = 0", + dbesc(normalise_link($contact_url))); + if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) { + $sql = "(`item`.`uid` = 0 OR (`item`.`uid` = %d AND `item`.`private`))"; + } else { + $sql = "`item`.`uid` = %d"; + } + + if (!dbm::is_result($r)) { + return ''; + } + + $author_id = intval($r[0]["author-id"]); + + if (get_config('system', 'old_pager')) { + $r = q("SELECT COUNT(*) AS `total` FROM `item` + WHERE `author-id` = %d and $sql", + intval($author_id), + intval(local_user())); + + $a->set_pager_total($r[0]['total']); + } + + $r = q(item_query()." AND `item`.`author-id` = %d AND ".$sql. + " ORDER BY `item`.`created` DESC LIMIT %d, %d", + intval($author_id), + intval(local_user()), + intval($a->pager['start']), + intval($a->pager['itemspage']) + ); + + $o = conversation($a,$r,'community',false); + + if (!get_config('system', 'old_pager')) { + $o .= alt_pager($a,count($r)); + } else { + $o .= paginate($a); + } + + return $o; +} + +/** + * @brief Returns a formatted location string from the given profile array + * + * @param array $profile Profile array (Generated from the "profile" table) + * + * @return string Location string + */ +function formatted_location($profile) { + $location = ''; + + if($profile['locality']) + $location .= $profile['locality']; + + if($profile['region'] AND ($profile['locality'] != $profile['region'])) { + if($location) + $location .= ', '; + + $location .= $profile['region']; + } + + if($profile['country-name']) { + if($location) + $location .= ', '; + + $location .= $profile['country-name']; + } + + return $location; +} + +/** + * @brief Returns the account type name + * + * The function can be called with either the user or the contact array + * + * @param array $contact contact or user array + */ +function account_type($contact) { + + // There are several fields that indicate that the contact or user is a forum + // "page-flags" is a field in the user table, + // "forum" and "prv" are used in the contact table. They stand for PAGE_COMMUNITY and PAGE_PRVGROUP. + // "community" is used in the gcontact table and is true if the contact is PAGE_COMMUNITY or PAGE_PRVGROUP. + if((isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_COMMUNITY)) + || (isset($contact['page-flags']) && (intval($contact['page-flags']) == PAGE_PRVGROUP)) + || (isset($contact['forum']) && intval($contact['forum'])) + || (isset($contact['prv']) && intval($contact['prv'])) + || (isset($contact['community']) && intval($contact['community']))) + $type = ACCOUNT_TYPE_COMMUNITY; + else + $type = ACCOUNT_TYPE_PERSON; + + // The "contact-type" (contact table) and "account-type" (user table) are more general then the chaos from above. + if (isset($contact["contact-type"])) + $type = $contact["contact-type"]; + if (isset($contact["account-type"])) + $type = $contact["account-type"]; + + switch($type) { + case ACCOUNT_TYPE_ORGANISATION: + $account_type = t("Organisation"); + break; + case ACCOUNT_TYPE_NEWS: + $account_type = t('News'); + break; + case ACCOUNT_TYPE_COMMUNITY: + $account_type = t("Forum"); + break; + default: + $account_type = ""; + break; + } + + return $account_type; +} +?> diff --git a/include/Core/Config.php b/include/Core/Config.php new file mode 100644 index 0000000000..4e5c1e3d04 --- /dev/null +++ b/include/Core/Config.php @@ -0,0 +1,216 @@ +config + * + * @param string $family + * The category of the configuration value + * @return void + */ + public static function load($family = "config") { + + // We don't preload "system" anymore. + // This reduces the number of database reads a lot. + if ($family === 'system') { + return; + } + + $a = get_app(); + + $r = q("SELECT `v`, `k` FROM `config` WHERE `cat` = '%s'", dbesc($family)); + if (dbm::is_result($r)) { + foreach ($r as $rr) { + $k = $rr['k']; + if ($family === 'config') { + $a->config[$k] = $rr['v']; + } else { + $a->config[$family][$k] = $rr['v']; + self::$cache[$family][$k] = $rr['v']; + self::$in_db[$family][$k] = true; + } + } + } + } + + /** + * @brief Get a particular user's config variable given the category name + * ($family) and a key. + * + * Get a particular config value from the given category ($family) + * and the $key from a cached storage in $a->config[$uid]. + * $instore is only used by the set_config function + * to determine if the key already exists in the DB + * If a key is found in the DB but doesn't exist in + * local config cache, pull it into the cache so we don't have + * to hit the DB again for this item. + * + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to query + * @param mixed $default_value optional + * The value to return if key is not set (default: null) + * @param boolean $refresh optional + * If true the config is loaded from the db and not from the cache (default: false) + * @return mixed Stored value or null if it does not exist + */ + public static function get($family, $key, $default_value = null, $refresh = false) { + + $a = get_app(); + + if (!$refresh) { + + // Do we have the cached value? Then return it + if (isset(self::$cache[$family][$key])) { + if (self::$cache[$family][$key] === '!!') { + return $default_value; + } else { + return self::$cache[$family][$key]; + } + } + } + + $ret = q("SELECT `v` FROM `config` WHERE `cat` = '%s' AND `k` = '%s'", + dbesc($family), + dbesc($key) + ); + if (dbm::is_result($ret)) { + // manage array value + $val = (preg_match("|^a:[0-9]+:{.*}$|s", $ret[0]['v']) ? unserialize($ret[0]['v']) : $ret[0]['v']); + + // Assign the value from the database to the cache + self::$cache[$family][$key] = $val; + self::$in_db[$family][$key] = true; + return $val; + } elseif (isset($a->config[$family][$key])) { + + // Assign the value (mostly) from the .htconfig.php to the cache + self::$cache[$family][$key] = $a->config[$family][$key]; + self::$in_db[$family][$key] = false; + + return $a->config[$family][$key]; + } + + self::$cache[$family][$key] = '!!'; + self::$in_db[$family][$key] = false; + + return $default_value; + } + + /** + * @brief Sets a configuration value for system config + * + * Stores a config value ($value) in the category ($family) under the key ($key) + * for the user_id $uid. + * + * Note: Please do not store booleans - convert to 0/1 integer values! + * + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to set + * @param string $value + * The value to store + * @return mixed Stored $value or false if the database update failed + */ + public static function set($family, $key, $value) { + $a = get_app(); + + // We store our setting values in a string variable. + // So we have to do the conversion here so that the compare below works. + // The exception are array values. + $dbvalue = (!is_array($value) ? (string)$value : $value); + + $stored = self::get($family, $key, null, true); + + if (($stored === $dbvalue) AND self::$in_db[$family][$key]) { + return true; + } + + if ($family === 'config') { + $a->config[$key] = $dbvalue; + } elseif ($family != 'system') { + $a->config[$family][$key] = $dbvalue; + } + + // Assign the just added value to the cache + self::$cache[$family][$key] = $dbvalue; + + // manage array value + $dbvalue = (is_array($value) ? serialize($value) : $dbvalue); + + if (is_null($stored) OR !self::$in_db[$family][$key]) { + $ret = q("INSERT INTO `config` (`cat`, `k`, `v`) VALUES ('%s', '%s', '%s') ON DUPLICATE KEY UPDATE `v` = '%s'", + dbesc($family), + dbesc($key), + dbesc($dbvalue), + dbesc($dbvalue) + ); + } else { + $ret = q("UPDATE `config` SET `v` = '%s' WHERE `cat` = '%s' AND `k` = '%s'", + dbesc($dbvalue), + dbesc($family), + dbesc($key) + ); + } + if ($ret) { + self::$in_db[$family][$key] = true; + return $value; + } + return $ret; + } + + /** + * @brief Deletes the given key from the system configuration. + * + * Removes the configured value from the stored cache in $a->config + * and removes it from the database. + * + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to delete + * @return mixed + */ + public static function delete($family, $key) { + + if (isset(self::$cache[$family][$key])) { + unset(self::$cache[$family][$key]); + unset(self::$in_db[$family][$key]); + } + $ret = q("DELETE FROM `config` WHERE `cat` = '%s' AND `k` = '%s'", + dbesc($family), + dbesc($key) + ); + + return $ret; + } +} diff --git a/include/Core/PConfig.php b/include/Core/PConfig.php new file mode 100644 index 0000000000..6ced9fc755 --- /dev/null +++ b/include/Core/PConfig.php @@ -0,0 +1,204 @@ +config[$uid]. + * + * @param string $uid + * The user_id + * @param string $family + * The category of the configuration value + * @return void + */ + public static function load($uid, $family) { + $a = get_app(); + $r = q("SELECT `v`,`k` FROM `pconfig` WHERE `cat` = '%s' AND `uid` = %d ORDER BY `cat`, `k`, `id`", + dbesc($family), + intval($uid) + ); + if (dbm::is_result($r)) { + foreach ($r as $rr) { + $k = $rr['k']; + $a->config[$uid][$family][$k] = $rr['v']; + self::$in_db[$uid][$family][$k] = true; + } + } else if ($family != 'config') { + // Negative caching + $a->config[$uid][$family] = "!!"; + } + } + + /** + * @brief Get a particular user's config variable given the category name + * ($family) and a key. + * + * Get a particular user's config value from the given category ($family) + * and the $key from a cached storage in $a->config[$uid]. + * + * @param string $uid + * The user_id + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to query + * @param mixed $default_value optional + * The value to return if key is not set (default: null) + * @param boolean $refresh optional + * If true the config is loaded from the db and not from the cache (default: false) + * @return mixed Stored value or null if it does not exist + */ + public static function get($uid, $family, $key, $default_value = null, $refresh = false) { + + $a = get_app(); + + if (!$refresh) { + // Looking if the whole family isn't set + if (isset($a->config[$uid][$family])) { + if ($a->config[$uid][$family] === '!!') { + return $default_value; + } + } + + if (isset($a->config[$uid][$family][$key])) { + if ($a->config[$uid][$family][$key] === '!!') { + return $default_value; + } + return $a->config[$uid][$family][$key]; + } + } + + $ret = q("SELECT `v` FROM `pconfig` WHERE `uid` = %d AND `cat` = '%s' AND `k` = '%s' ORDER BY `id` DESC LIMIT 1", + intval($uid), + dbesc($family), + dbesc($key) + ); + + if (count($ret)) { + $val = (preg_match("|^a:[0-9]+:{.*}$|s", $ret[0]['v'])?unserialize( $ret[0]['v']):$ret[0]['v']); + $a->config[$uid][$family][$key] = $val; + self::$in_db[$uid][$family][$key] = true; + + return $val; + } else { + $a->config[$uid][$family][$key] = '!!'; + self::$in_db[$uid][$family][$key] = false; + + return $default_value; + } + } + + /** + * @brief Sets a configuration value for a user + * + * Stores a config value ($value) in the category ($family) under the key ($key) + * for the user_id $uid. + * + * @note Please do not store booleans - convert to 0/1 integer values! + * + * @param string $uid + * The user_id + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to set + * @param string $value + * The value to store + * @return mixed Stored $value or false + */ + public static function set($uid, $family, $key, $value) { + + $a = get_app(); + + // We store our setting values in a string variable. + // So we have to do the conversion here so that the compare below works. + // The exception are array values. + $dbvalue = (!is_array($value) ? (string)$value : $value); + + $stored = self::get($uid, $family, $key, null, true); + + if (($stored === $dbvalue) AND self::$in_db[$uid][$family][$key]) { + return true; + } + + $a->config[$uid][$family][$key] = $dbvalue; + + // manage array value + $dbvalue = (is_array($value) ? serialize($value) : $dbvalue); + + if (is_null($stored) OR !self::$in_db[$uid][$family][$key]) { + $ret = q("INSERT INTO `pconfig` (`uid`, `cat`, `k`, `v`) VALUES (%d, '%s', '%s', '%s') ON DUPLICATE KEY UPDATE `v` = '%s'", + intval($uid), + dbesc($family), + dbesc($key), + dbesc($dbvalue), + dbesc($dbvalue) + ); + } else { + $ret = q("UPDATE `pconfig` SET `v` = '%s' WHERE `uid` = %d AND `cat` = '%s' AND `k` = '%s'", + dbesc($dbvalue), + intval($uid), + dbesc($family), + dbesc($key) + ); + } + + if ($ret) { + self::$in_db[$uid][$family][$key] = true; + return $value; + } + return $ret; + } + + /** + * @brief Deletes the given key from the users's configuration. + * + * Removes the configured value from the stored cache in $a->config[$uid] + * and removes it from the database. + * + * @param string $uid The user_id + * @param string $family + * The category of the configuration value + * @param string $key + * The configuration key to delete + * @return mixed + */ + public static function delete($uid,$family,$key) { + + $a = get_app(); + + if (x($a->config[$uid][$family], $key)) { + unset($a->config[$uid][$family][$key]); + unset(self::$in_db[$uid][$family][$key]); + } + + $ret = q("DELETE FROM `pconfig` WHERE `uid` = %d AND `cat` = '%s' AND `k` = '%s'", + intval($uid), + dbesc($family), + dbesc($key) + ); + + return $ret; + } +} diff --git a/include/DirSearch.php b/include/DirSearch.php new file mode 100644 index 0000000000..5968608236 --- /dev/null +++ b/include/DirSearch.php @@ -0,0 +1,63 @@ + 0 OR (NOT `gcontact`.`hide` AND `gcontact`.`network` IN ('%s', '%s', '%s') AND + ((`gcontact`.`last_contact` >= `gcontact`.`last_failure`) OR (`gcontact`.`updated` >= `gcontact`.`last_failure`)))) AND + (`gcontact`.`addr` LIKE '%s' OR `gcontact`.`name` LIKE '%s' OR `gcontact`.`nick` LIKE '%s') $extra_sql + GROUP BY `gcontact`.`nurl` + ORDER BY `gcontact`.`nurl` DESC + LIMIT 1000", + intval(local_user()), dbesc(CONTACT_IS_SHARING), dbesc(CONTACT_IS_FRIEND), + dbesc(NETWORK_DFRN), dbesc($ostatus), dbesc($diaspora), + dbesc(escape_tags($search)), dbesc(escape_tags($search)), dbesc(escape_tags($search))); + + return $results; + } + + } +} diff --git a/include/Emailer.php b/include/Emailer.php index d0568f6001..b0cdc3fe63 100644 --- a/include/Emailer.php +++ b/include/Emailer.php @@ -30,8 +30,8 @@ class Emailer { // generate a mime boundary $mimeBoundary =rand(0,9)."-" - .rand(10000000000,99999999999)."-" - .rand(10000000000,99999999999)."=:" + .rand(100000000,999999999)."-" + .rand(100000000,999999999)."=:" .rand(10000,99999); // generate a multipart/alternative message header diff --git a/include/ForumManager.php b/include/ForumManager.php new file mode 100644 index 0000000000..c2a20df29f --- /dev/null +++ b/include/ForumManager.php @@ -0,0 +1,192 @@ + forum url + * 'name' => forum name + * 'id' => number of the key from the array + * 'micro' => contact photo in format micro + * 'thumb' => contact photo in format thumb + */ + public static function get_list($uid, $showhidden = true, $lastitem, $showprivate = false) { + + $forumlist = array(); + + $order = (($showhidden) ? '' : ' AND NOT `hidden` '); + $order .= (($lastitem) ? ' ORDER BY `last-item` DESC ' : ' ORDER BY `name` ASC '); + $select = '`forum` '; + if ($showprivate) { + $select = '(`forum` OR `prv`)'; + } + + $contacts = q("SELECT `contact`.`id`, `contact`.`url`, `contact`.`name`, `contact`.`micro`, `contact`.`thumb` FROM `contact` + WHERE `network`= 'dfrn' AND $select AND `uid` = %d + AND NOT `blocked` AND NOT `hidden` AND NOT `pending` AND NOT `archive` + AND `success_update` > `failure_update` + $order ", + intval($uid) + ); + + if (!$contacts) + return($forumlist); + + foreach($contacts as $contact) { + $forumlist[] = array( + 'url' => $contact['url'], + 'name' => $contact['name'], + 'id' => $contact['id'], + 'micro' => $contact['micro'], + 'thumb' => $contact['thumb'], + ); + } + return($forumlist); + } + + + /** + * @brief Forumlist widget + * + * Sidebar widget to show subcribed friendica forums. If activated + * in the settings, it appears at the notwork page sidebar + * + * @param int $uid The ID of the User + * @param int $cid + * The contact id which is used to mark a forum as "selected" + * @return string + */ + public static function widget($uid,$cid = 0) { + + if(! intval(feature_enabled(local_user(),'forumlist_widget'))) + return; + + $o = ''; + + //sort by last updated item + $lastitem = true; + + $contacts = self::get_list($uid,true,$lastitem, true); + $total = count($contacts); + $visible_forums = 10; + + if (dbm::is_result($contacts)) { + + $id = 0; + + foreach($contacts as $contact) { + + $selected = (($cid == $contact['id']) ? ' forum-selected' : ''); + + $entry = array( + 'url' => 'network?f=&cid=' . $contact['id'], + 'external_url' => 'redir/' . $contact['id'], + 'name' => $contact['name'], + 'cid' => $contact['id'], + 'selected' => $selected, + 'micro' => App::remove_baseurl(proxy_url($contact['micro'], false, PROXY_SIZE_MICRO)), + 'id' => ++$id, + ); + $entries[] = $entry; + } + + $tpl = get_markup_template('widget_forumlist.tpl'); + + $o .= replace_macros($tpl,array( + '$title' => t('Forums'), + '$forums' => $entries, + '$link_desc' => t('External link to forum'), + '$total' => $total, + '$visible_forums' => $visible_forums, + '$showmore' => t('show more'), + )); + } + + return $o; + } + + /** + * @brief Format forumlist as contact block + * + * This function is used to show the forumlist in + * the advanced profile. + * + * @param int $uid The ID of the User + * @return string + * + */ + public static function profile_advanced($uid) { + + $profile = intval(feature_enabled($uid,'forumlist_profile')); + if(! $profile) + return; + + $o = ''; + + // place holder in case somebody wants configurability + $show_total = 9999; + + //don't sort by last updated item + $lastitem = false; + + $contacts = self::get_list($uid,false,$lastitem,false); + + $total_shown = 0; + + foreach($contacts as $contact) { + $forumlist .= micropro($contact,false,'forumlist-profile-advanced'); + $total_shown ++; + if($total_shown == $show_total) + break; + } + + if(count($contacts) > 0) + $o .= $forumlist; + return $o; + } + + /** + * @brief count unread forum items + * + * Count unread items of connected forums and private groups + * + * @return array + * 'id' => contact id + * 'name' => contact/forum name + * 'count' => counted unseen forum items + * + */ + public static function count_unseen_items() { + $r = q("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` + AND `contact`.`network`= 'dfrn' AND (`contact`.`forum` OR `contact`.`prv`) + AND NOT `contact`.`blocked` AND NOT `contact`.`hidden` + AND NOT `contact`.`pending` AND NOT `contact`.`archive` + AND `contact`.`success_update` > `failure_update` + GROUP BY `contact`.`id` ", + intval(local_user()) + ); + + return $r; + } + +} diff --git a/include/HTTPExceptions.php b/include/HTTPExceptions.php new file mode 100644 index 0000000000..8571c99de5 --- /dev/null +++ b/include/HTTPExceptions.php @@ -0,0 +1,105 @@ +httpdesc=="") { + $this->httpdesc = preg_replace("|([a-z])([A-Z])|",'$1 $2', str_replace("Exception","",get_class($this))); + } + parent::__construct($message, $code, $previous); + } +} + +// 4xx +class TooManyRequestsException extends HTTPException { + var $httpcode = 429; +} + +class UnauthorizedException extends HTTPException { + var $httpcode = 401; +} + +class ForbiddenException extends HTTPException { + var $httpcode = 403; +} + +class NotFoundException extends HTTPException { + var $httpcode = 404; +} + +class GoneException extends HTTPException { + var $httpcode = 410; +} + +class MethodNotAllowedException extends HTTPException { + var $httpcode = 405; +} + +class NonAcceptableException extends HTTPException { + var $httpcode = 406; +} + +class LenghtRequiredException extends HTTPException { + var $httpcode = 411; +} + +class PreconditionFailedException extends HTTPException { + var $httpcode = 412; +} + +class UnsupportedMediaTypeException extends HTTPException { + var $httpcode = 415; +} + +class ExpetationFailesException extends HTTPException { + var $httpcode = 417; +} + +class ConflictException extends HTTPException { + var $httpcode = 409; +} + +class UnprocessableEntityException extends HTTPException { + var $httpcode = 422; +} + +class ImATeapotException extends HTTPException { + var $httpcode = 418; + var $httpdesc = "I'm A Teapot"; +} + +class BadRequestException extends HTTPException { + var $httpcode = 400; +} + +// 5xx + +class ServiceUnavaiableException extends HTTPException { + var $httpcode = 503; +} + +class BadGatewayException extends HTTPException { + var $httpcode = 502; +} + +class GatewayTimeoutException extends HTTPException { + var $httpcode = 504; +} + +class NotImplementedException extends HTTPException { + var $httpcode = 501; +} + +class InternalServerErrorException extends HTTPException { + var $httpcode = 500; +} + + + diff --git a/include/NotificationsManager.php b/include/NotificationsManager.php new file mode 100644 index 0000000000..611860f9d0 --- /dev/null +++ b/include/NotificationsManager.php @@ -0,0 +1,830 @@ +a = get_app(); + } + + /** + * @brief set some extra note properties + * + * @param array $notes array of note arrays from db + * @return array Copy of input array with added properties + * + * Set some extra properties to note array from db: + * - timestamp as int in default TZ + * - date_rel : relative date string + * - msg_html: message as html string + * - msg_plain: message as plain text string + */ + private function _set_extra($notes) { + $rets = array(); + foreach($notes as $n) { + $local_time = datetime_convert('UTC',date_default_timezone_get(),$n['date']); + $n['timestamp'] = strtotime($local_time); + $n['date_rel'] = relative_date($n['date']); + $n['msg_html'] = bbcode($n['msg'], false, false, false, false); + $n['msg_plain'] = explode("\n",trim(html2plain($n['msg_html'], 0)))[0]; + + $rets[] = $n; + } + return $rets; + } + + + /** + * @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 string $limit optional Query limits + * + * @return array of results or false on errors + */ + public function getAll($filter = array(), $order="-date", $limit="") { + $filter_str = array(); + $filter_sql = ""; + foreach($filter as $column => $value) { + $filter_str[] = sprintf("`%s` = '%s'", $column, dbesc($value)); + } + if (count($filter_str)>0) { + $filter_sql = "AND ".implode(" AND ", $filter_str); + } + + $aOrder = explode(" ", $order); + $asOrder = array(); + 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); + + if($limit!="") + $limit = " LIMIT ".$limit; + + $r = q("SELECT * FROM `notify` WHERE `uid` = %d $filter_sql ORDER BY $order_sql $limit", + intval(local_user()) + ); + + if (dbm::is_result($r)) + return $this->_set_extra($r); + + return false; + } + + /** + * @brief Get one note for local_user() by $id value + * + * @param int $id + * @return array note values or null if not found + */ + public function getByID($id) { + $r = q("SELECT * FROM `notify` WHERE `id` = %d AND `uid` = %d LIMIT 1", + intval($id), + intval(local_user()) + ); + if (dbm::is_result($r)) { + return $this->_set_extra($r)[0]; + } + return null; + } + + /** + * @brief set seen state of $note of local_user() + * + * @param array $note + * @param bool $seen optional true or false, default true + * @return bool true on success, false on errors + */ + 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), + dbesc($note['link']), + intval($note['parent']), + dbesc($note['otype']), + intval(local_user()) + ); + } + + /** + * @brief set seen state of all notifications of local_user() + * + * @param bool $seen optional true or false. default true + * @return bool true on success, false on error + */ + public function setAllSeen($seen = true) { + return q("UPDATE `notify` SET `seen` = %d WHERE `uid` = %d", + intval($seen), + intval(local_user()) + ); + } + + /** + * @brief List of pages for the Notifications TabBar + * + * @param app $a The + * @return array with with notifications TabBar data + */ + public function getTabs() { + $tabs = array( + array( + 'label' => t('System'), + 'url'=>'notifications/system', + 'sel'=> (($this->a->argv[1] == 'system') ? 'active' : ''), + 'id' => 'system-tab', + 'accesskey' => 'y', + ), + array( + 'label' => t('Network'), + 'url'=>'notifications/network', + 'sel'=> (($this->a->argv[1] == 'network') ? 'active' : ''), + 'id' => 'network-tab', + 'accesskey' => 'w', + ), + array( + 'label' => t('Personal'), + 'url'=>'notifications/personal', + 'sel'=> (($this->a->argv[1] == 'personal') ? 'active' : ''), + 'id' => 'personal-tab', + 'accesskey' => 'r', + ), + array( + 'label' => t('Home'), + 'url' => 'notifications/home', + 'sel'=> (($this->a->argv[1] == 'home') ? 'active' : ''), + 'id' => 'home-tab', + 'accesskey' => 'h', + ), + array( + 'label' => t('Introductions'), + 'url' => 'notifications/intros', + 'sel'=> (($this->a->argv[1] == 'intros') ? 'active' : ''), + 'id' => 'intro-tab', + 'accesskey' => 'i', + ), + ); + + return $tabs; + } + + /** + * @brief Format the notification query in an usable array + * + * @param array $notifs The array from the db query + * @param string $ident The notifications identifier (e.g. network) + * @return array + * string 'label' => The type of the notification + * string 'link' => URL to the source + * string 'image' => The avatar image + * string 'url' => The profile url of the contact + * string 'text' => The notification text + * string 'when' => The date of the notification + * string 'ago' => T relative date of the notification + * bool 'seen' => Is the notification marked as "seen" + */ + private function formatNotifs($notifs, $ident = "") { + + $notif = array(); + $arr = array(); + + if (dbm::is_result($notifs)) { + + foreach ($notifs as $it) { + // Because we use different db tables for the notification query + // we have sometimes $it['unseen'] and sometimes $it['seen]. + // So we will have to transform $it['unseen'] + if (array_key_exists('unseen', $it)) { + $it['seen'] = ($it['unseen'] > 0 ? false : true); + } + + // Depending on the identifier of the notification we need to use different defaults + switch ($ident) { + case 'system': + $default_item_label = 'notify'; + $default_item_link = $this->a->get_baseurl(true).'/notify/view/'. $it['id']; + $default_item_image = proxy_url($it['photo'], false, PROXY_SIZE_MICRO); + $default_item_url = $it['url']; + $default_item_text = strip_tags(bbcode($it['msg'])); + $default_item_when = datetime_convert('UTC', date_default_timezone_get(), $it['date'], 'r'); + $default_item_ago = relative_date($it['date']); + break; + + case 'home': + $default_item_label = 'comment'; + $default_item_link = $this->a->get_baseurl(true).'/display/'.$it['pguid']; + $default_item_image = proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO); + $default_item_url = $it['author-link']; + $default_item_text = sprintf(t("%s commented on %s's post"), $it['author-name'], $it['pname']); + $default_item_when = datetime_convert('UTC', date_default_timezone_get(), $it['created'], 'r'); + $default_item_ago = relative_date($it['created']); + break; + + default: + $default_item_label = (($it['id'] == $it['parent']) ? 'post' : 'comment'); + $default_item_link = $this->a->get_baseurl(true).'/display/'.$it['pguid']; + $default_item_image = proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO); + $default_item_url = $it['author-link']; + $default_item_text = (($it['id'] == $it['parent']) + ? sprintf(t("%s created a new post"), $it['author-name']) + : sprintf(t("%s commented on %s's post"), $it['author-name'], $it['pname'])); + $default_item_when = datetime_convert('UTC', date_default_timezone_get(), $it['created'], 'r'); + $default_item_ago = relative_date($it['created']); + + } + + // Transform the different types of notification in an usable array + switch ($it['verb']){ + case ACTIVITY_LIKE: + $notif = array( + 'label' => 'like', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf(t("%s liked %s's post"), $it['author-name'], $it['pname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + case ACTIVITY_DISLIKE: + $notif = array( + 'label' => 'dislike', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf(t("%s disliked %s's post"), $it['author-name'], $it['pname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + case ACTIVITY_ATTEND: + $notif = array( + 'label' => 'attend', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf(t("%s is attending %s's event"), $it['author-name'], $it['pname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + case ACTIVITY_ATTENDNO: + $notif = array( + 'label' => 'attendno', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf( t("%s is not attending %s's event"), $it['author-name'], $it['pname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + case ACTIVITY_ATTENDMAYBE: + $notif = array( + 'label' => 'attendmaybe', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf(t("%s may attend %s's event"), $it['author-name'], $it['pname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + case ACTIVITY_FRIEND: + $xmlhead="<"."?xml version='1.0' encoding='UTF-8' ?".">"; + $obj = parse_xml_string($xmlhead.$it['object']); + $it['fname'] = $obj->title; + + $notif = array( + 'label' => 'friend', + 'link' => $this->a->get_baseurl(true).'/display/'.$it['pguid'], + 'image' => proxy_url($it['author-avatar'], false, PROXY_SIZE_MICRO), + 'url' => $it['author-link'], + 'text' => sprintf(t("%s is now friends with %s"), $it['author-name'], $it['fname']), + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + break; + + default: + $notif = array( + 'label' => $default_item_label, + 'link' => $default_item_link, + 'image' => $default_item_image, + 'url' => $default_item_url, + 'text' => $default_item_text, + 'when' => $default_item_when, + 'ago' => $default_item_ago, + 'seen' => $it['seen'] + ); + } + + $arr[] = $notif; + } + } + + return $arr; + + } + + /** + * @brief Total number of network notifications + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @return int Number of network notifications + */ + private function networkTotal($seen = 0) { + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + $r = q("SELECT COUNT(*) AS `total` + FROM `item` INNER JOIN `item` AS `pitem` ON `pitem`.`id`=`item`.`parent` + WHERE `item`.`visible` = 1 AND `pitem`.`parent` != 0 AND + `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 0 + $sql_seen", + intval(local_user()) + ); + + if (dbm::is_result($r)) + return $r[0]['total']; + + return 0; + } + + /** + * @brief Get network notifications + * + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @param int $start Start the query at this point + * @param int $limit Maximum number of query results + * + * @return array with + * string 'ident' => Notification identifier + * int 'total' => Total number of available network notifications + * array 'notifications' => Network notifications + */ + public function networkNotifs($seen = 0, $start = 0, $limit = 80) { + $ident = 'network'; + $total = $this->networkTotal($seen); + $notifs = array(); + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + + $r = q("SELECT `item`.`id`,`item`.`parent`, `item`.`verb`, `item`.`author-name`, `item`.`unseen`, + `item`.`author-link`, `item`.`author-avatar`, `item`.`created`, `item`.`object` AS `object`, + `pitem`.`author-name` AS `pname`, `pitem`.`author-link` AS `plink`, `pitem`.`guid` AS `pguid` + FROM `item` INNER JOIN `item` AS `pitem` ON `pitem`.`id`=`item`.`parent` + WHERE `item`.`visible` = 1 AND `pitem`.`parent` != 0 AND + `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 0 + $sql_seen + ORDER BY `item`.`created` DESC LIMIT %d, %d ", + intval(local_user()), + intval($start), + intval($limit) + ); + + if (dbm::is_result($r)) + $notifs = $this->formatNotifs($r, $ident); + + $arr = array ( + 'notifications' => $notifs, + 'ident' => $ident, + 'total' => $total, + ); + + return $arr; + } + + /** + * @brief Total number of system notifications + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @return int Number of system notifications + */ + private function systemTotal($seen = 0) { + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `seen` = 0 "; + + $r = q("SELECT COUNT(*) AS `total` FROM `notify` WHERE `uid` = %d $sql_seen", + intval(local_user()) + ); + + if (dbm::is_result($r)) + return $r[0]['total']; + + return 0; + } + + /** + * @brief Get system notifications + * + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @param int $start Start the query at this point + * @param int $limit Maximum number of query results + * + * @return array with + * string 'ident' => Notification identifier + * int 'total' => Total number of available system notifications + * array 'notifications' => System notifications + */ + public function systemNotifs($seen = 0, $start = 0, $limit = 80) { + $ident = 'system'; + $total = $this->systemTotal($seen); + $notifs = array(); + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `seen` = 0 "; + + $r = q("SELECT `id`, `url`, `photo`, `msg`, `date`, `seen` FROM `notify` + WHERE `uid` = %d $sql_seen ORDER BY `date` DESC LIMIT %d, %d ", + intval(local_user()), + intval($start), + intval($limit) + ); + + if (dbm::is_result($r)) + $notifs = $this->formatNotifs($r, $ident); + + $arr = array ( + 'notifications' => $notifs, + 'ident' => $ident, + 'total' => $total, + ); + + return $arr; + } + + /** + * @brief Addional SQL query string for the personal notifications + * + * @return string The additional sql query + */ + private function _personal_sql_extra() { + $myurl = $this->a->get_baseurl(true) . '/profile/'. $this->a->user['nickname']; + $myurl = substr($myurl,strpos($myurl,'://')+3); + $myurl = str_replace(array('www.','.'),array('','\\.'),$myurl); + $diasp_url = str_replace('/profile/','/u/',$myurl); + $sql_extra = sprintf(" AND ( `item`.`author-link` regexp '%s' or `item`.`tag` regexp '%s' or `item`.`tag` regexp '%s' ) ", + dbesc($myurl . '$'), + dbesc($myurl . '\\]'), + dbesc($diasp_url . '\\]') + ); + + return $sql_extra; + } + + /** + * @brief Total number of personal notifications + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @return int Number of personal notifications + */ + private function personalTotal($seen = 0) { + $sql_seen = ""; + $sql_extra = $this->_personal_sql_extra(); + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + $r = q("SELECT COUNT(*) AS `total` + FROM `item` INNER JOIN `item` AS `pitem` ON `pitem`.`id`=`item`.`parent` + WHERE `item`.`visible` = 1 + $sql_extra + $sql_seen + AND `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 0 " , + intval(local_user()) + ); + + if (dbm::is_result($r)) + return $r[0]['total']; + + return 0; + } + + /** + * @brief Get personal notifications + * + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @param int $start Start the query at this point + * @param int $limit Maximum number of query results + * + * @return array with + * string 'ident' => Notification identifier + * int 'total' => Total number of available personal notifications + * array 'notifications' => Personal notifications + */ + public function personalNotifs($seen = 0, $start = 0, $limit = 80) { + $ident = 'personal'; + $total = $this->personalTotal($seen); + $sql_extra = $this->_personal_sql_extra(); + $notifs = array(); + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + $r = q("SELECT `item`.`id`,`item`.`parent`, `item`.`verb`, `item`.`author-name`, `item`.`unseen`, + `item`.`author-link`, `item`.`author-avatar`, `item`.`created`, `item`.`object` AS `object`, + `pitem`.`author-name` AS `pname`, `pitem`.`author-link` AS `plink`, `pitem`.`guid` AS `pguid` + FROM `item` INNER JOIN `item` AS `pitem` ON `pitem`.`id`=`item`.`parent` + WHERE `item`.`visible` = 1 + $sql_extra + $sql_seen + AND `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 0 + ORDER BY `item`.`created` DESC LIMIT %d, %d " , + intval(local_user()), + intval($start), + intval($limit) + ); + + if (dbm::is_result($r)) + $notifs = $this->formatNotifs($r, $ident); + + $arr = array ( + 'notifications' => $notifs, + 'ident' => $ident, + 'total' => $total, + ); + + return $arr; + } + + /** + * @brief Total number of home notifications + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @return int Number of home notifications + */ + private function homeTotal($seen = 0) { + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + $r = q("SELECT COUNT(*) AS `total` FROM `item` + WHERE `item`.`visible` = 1 AND + `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 1 + $sql_seen", + intval(local_user()) + ); + + if (dbm::is_result($r)) + return $r[0]['total']; + + return 0; + } + + /** + * @brief Get home notifications + * + * @param int|string $seen + * If 0 only include notifications into the query + * which aren't marked as "seen" + * @param int $start Start the query at this point + * @param int $limit Maximum number of query results + * + * @return array with + * string 'ident' => Notification identifier + * int 'total' => Total number of available home notifications + * array 'notifications' => Home notifications + */ + public function homeNotifs($seen = 0, $start = 0, $limit = 80) { + $ident = 'home'; + $total = $this->homeTotal($seen); + $notifs = array(); + $sql_seen = ""; + + if($seen === 0) + $sql_seen = " AND `item`.`unseen` = 1 "; + + $r = q("SELECT `item`.`id`,`item`.`parent`, `item`.`verb`, `item`.`author-name`, `item`.`unseen`, + `item`.`author-link`, `item`.`author-avatar`, `item`.`created`, `item`.`object` AS `object`, + `pitem`.`author-name` AS `pname`, `pitem`.`author-link` AS `plink`, `pitem`.`guid` AS `pguid` + FROM `item` INNER JOIN `item` AS `pitem` ON `pitem`.`id`=`item`.`parent` + WHERE `item`.`visible` = 1 AND + `item`.`deleted` = 0 AND `item`.`uid` = %d AND `item`.`wall` = 1 + $sql_seen + ORDER BY `item`.`created` DESC LIMIT %d, %d ", + intval(local_user()), + intval($start), + intval($limit) + ); + + if (dbm::is_result($r)) + $notifs = $this->formatNotifs($r, $ident); + + $arr = array ( + 'notifications' => $notifs, + 'ident' => $ident, + 'total' => $total, + ); + + return $arr; + } + + /** + * @brief Total number of introductions + * @param bool $all + * If false only include introductions into the query + * which aren't marked as ignored + * @return int Number of introductions + */ + private function introTotal($all = false) { + $sql_extra = ""; + + if(!$all) + $sql_extra = " AND `ignore` = 0 "; + + $r = q("SELECT COUNT(*) AS `total` FROM `intro` + WHERE `intro`.`uid` = %d $sql_extra AND `intro`.`blocked` = 0 ", + intval($_SESSION['uid']) + ); + + if (dbm::is_result($r)) + return $r[0]['total']; + + return 0; + } + + /** + * @brief Get introductions + * + * @param bool $all + * If false only include introductions into the query + * which aren't marked as ignored + * @param int $start Start the query at this point + * @param int $limit Maximum number of query results + * + * @return array with + * string 'ident' => Notification identifier + * int 'total' => Total number of available introductions + * array 'notifications' => Introductions + */ + public function introNotifs($all = false, $start = 0, $limit = 80) { + $ident = 'introductions'; + $total = $this->introTotal($seen); + $notifs = array(); + $sql_extra = ""; + + if(!$all) + $sql_extra = " AND `ignore` = 0 "; + + /// @todo Fetch contact details by "get_contact_details_by_url" instead of queries to contact, fcontact and gcontact + $r = q("SELECT `intro`.`id` AS `intro_id`, `intro`.*, `contact`.*, `fcontact`.`name` AS `fname`,`fcontact`.`url` AS `furl`,`fcontact`.`photo` AS `fphoto`,`fcontact`.`request` AS `frequest`, + `gcontact`.`location` AS `glocation`, `gcontact`.`about` AS `gabout`, + `gcontact`.`keywords` AS `gkeywords`, `gcontact`.`gender` AS `ggender`, + `gcontact`.`network` AS `gnetwork` + FROM `intro` + LEFT JOIN `contact` ON `contact`.`id` = `intro`.`contact-id` + LEFT JOIN `gcontact` ON `gcontact`.`nurl` = `contact`.`nurl` + LEFT JOIN `fcontact` ON `intro`.`fid` = `fcontact`.`id` + WHERE `intro`.`uid` = %d $sql_extra AND `intro`.`blocked` = 0 + LIMIT %d, %d", + intval($_SESSION['uid']), + intval($start), + intval($limit) + ); + + if (dbm::is_result($r)) + $notifs = $this->formatIntros($r); + + $arr = array ( + 'ident' => $ident, + 'total' => $total, + 'notifications' => $notifs, + ); + + return $arr; + } + + /** + * @brief Format the notification query in an usable array + * + * @param array $intros The array from the db query + * @return array with the introductions + */ + private function formatIntros($intros) { + $knowyou = ''; + + foreach($intros as $it) { + // There are two kind of introduction. Contacts suggested by other contacts and normal connection requests. + // We have to distinguish between these two because they use different data. + + // Contact suggestions + if($it['fid']) { + + $return_addr = bin2hex($this->a->user['nickname'] . '@' . $this->a->get_hostname() . (($this->a->path) ? '/' . $this->a->path : '')); + + $intro = array( + 'label' => 'friend_suggestion', + 'notify_type' => t('Friend Suggestion'), + 'intro_id' => $it['intro_id'], + 'madeby' => $it['name'], + 'contact_id' => $it['contact-id'], + 'photo' => ((x($it,'fphoto')) ? proxy_url($it['fphoto'], false, PROXY_SIZE_SMALL) : "images/person-175.jpg"), + 'name' => $it['fname'], + 'url' => zrl($it['furl']), + 'hidden' => $it['hidden'] == 1, + 'post_newfriend' => (intval(get_pconfig(local_user(),'system','post_newfriend')) ? '1' : 0), + + 'knowyou' => $knowyou, + 'note' => $it['note'], + 'request' => $it['frequest'] . '?addr=' . $return_addr, + + ); + + // Normal connection requests + } else { + + // Probe the contact url to get missing data + $ret = probe_url($it["url"]); + + if ($it['gnetwork'] == "") + $it['gnetwork'] = $ret["network"]; + + // Don't show these data until you are connected. Diaspora is doing the same. + if($it['gnetwork'] === NETWORK_DIASPORA) { + $it['glocation'] = ""; + $it['gabout'] = ""; + $it['ggender'] = ""; + } + $intro = array( + 'label' => (($it['network'] !== NETWORK_OSTATUS) ? 'friend_request' : 'follower'), + 'notify_type' => (($it['network'] !== NETWORK_OSTATUS) ? t('Friend/Connect Request') : t('New Follower')), + 'dfrn_id' => $it['issued-id'], + 'uid' => $_SESSION['uid'], + 'intro_id' => $it['intro_id'], + 'contact_id' => $it['contact-id'], + 'photo' => ((x($it,'photo')) ? proxy_url($it['photo'], false, PROXY_SIZE_SMALL) : "images/person-175.jpg"), + 'name' => $it['name'], + 'location' => bbcode($it['glocation'], false, false), + 'about' => bbcode($it['gabout'], false, false), + 'keywords' => $it['gkeywords'], + 'gender' => $it['ggender'], + 'hidden' => $it['hidden'] == 1, + 'post_newfriend' => (intval(get_pconfig(local_user(),'system','post_newfriend')) ? '1' : 0), + 'url' => $it['url'], + 'zrl' => zrl($it['url']), + 'addr' => $ret['addr'], + 'network' => $it['gnetwork'], + 'knowyou' => $it['knowyou'], + 'note' => $it['note'], + ); + } + + $arr[] = $intro; + } + + return $arr; + } +} diff --git a/include/ParseUrl.php b/include/ParseUrl.php new file mode 100644 index 0000000000..e9ac527a1a --- /dev/null +++ b/include/ParseUrl.php @@ -0,0 +1,532 @@ + The url of the parsed page + * string 'type' => Content type + * string 'title' => The title of the content + * string 'text' => The description for the content + * string 'image' => A preview image of the content (only available + * if $no_geuessing = false + * array'images' = Array of preview pictures + * string 'keywords' => The tags which belong to the content + * + * @see ParseUrl::getSiteinfo() for more information about scraping + * embeddable content + */ + public static function getSiteinfoCached($url, $no_guessing = false, $do_oembed = true) { + + if ($url == "") { + return false; + } + + $r = q("SELECT * FROM `parsed_url` WHERE `url` = '%s' AND `guessing` = %d AND `oembed` = %d", + dbesc(normalise_link($url)), intval(!$no_guessing), intval($do_oembed)); + + if ($r) { + $data = $r[0]["content"]; + } + + if (!is_null($data)) { + $data = unserialize($data); + return $data; + } + + $data = self::getSiteinfo($url, $no_guessing, $do_oembed); + + q("INSERT INTO `parsed_url` (`url`, `guessing`, `oembed`, `content`, `created`) VALUES ('%s', %d, %d, '%s', '%s') + ON DUPLICATE KEY UPDATE `content` = '%s', `created` = '%s'", + dbesc(normalise_link($url)), intval(!$no_guessing), intval($do_oembed), + dbesc(serialize($data)), dbesc(datetime_convert()), + dbesc(serialize($data)), dbesc(datetime_convert())); + + return $data; + } + /** + * @brief Parse a page for embeddable content information + * + * This method parses to url for meta data which can be used to embed + * the content. If available it prioritizes Open Graph meta tags. + * If this is not available it uses the twitter cards meta tags. + * As fallback it uses standard html elements with meta informations + * like \Awesome Title\ or + * \ + * + * @param type $url The url of the page which should be scraped + * @param type $no_guessing If true the parse doens't search for + * preview pictures + * @param type $do_oembed The false option is used by the function fetch_oembed() + * to avoid endless loops + * @param type $count Internal counter to avoid endless loops + * + * @return array which contains needed data for embedding + * string 'url' => The url of the parsed page + * string 'type' => Content type + * string 'title' => The title of the content + * string 'text' => The description for the content + * string 'image' => A preview image of the content (only available + * if $no_geuessing = false + * array'images' = Array of preview pictures + * string 'keywords' => The tags which belong to the content + * + * @todo https://developers.google.com/+/plugins/snippet/ + * @verbatim + * + * + * + * + * + *

Shiny Trinket

+ * + *

Shiny trinkets are shiny.

+ * + * @endverbatim + */ + public static function getSiteinfo($url, $no_guessing = false, $do_oembed = true, $count = 1) { + + $a = get_app(); + + $siteinfo = array(); + + // Check if the URL does contain a scheme + $scheme = parse_url($url, PHP_URL_SCHEME); + + if ($scheme == "") { + $url = "http://".trim($url, "/"); + } + + if ($count > 10) { + logger("parseurl_getsiteinfo: Endless loop detected for ".$url, LOGGER_DEBUG); + return($siteinfo); + } + + $url = trim($url, "'"); + $url = trim($url, '"'); + + $url = strip_tracking_query_params($url); + + $siteinfo["url"] = $url; + $siteinfo["type"] = "link"; + + $check_cert = Config::get("system", "verifyssl"); + + $stamp1 = microtime(true); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_USERAGENT, $a->get_useragent()); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, (($check_cert) ? true : false)); + if ($check_cert) { + @curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + } + + $header = curl_exec($ch); + $curl_info = @curl_getinfo($ch); + curl_close($ch); + + $a->save_timestamp($stamp1, "network"); + + if ((($curl_info["http_code"] == "301") || ($curl_info["http_code"] == "302") || ($curl_info["http_code"] == "303") || ($curl_info["http_code"] == "307")) + && (($curl_info["redirect_url"] != "") || ($curl_info["location"] != ""))) { + if ($curl_info["redirect_url"] != "") { + $siteinfo = self::getSiteinfo($curl_info["redirect_url"], $no_guessing, $do_oembed, ++$count); + } else { + $siteinfo = self::getSiteinfo($curl_info["location"], $no_guessing, $do_oembed, ++$count); + } + return($siteinfo); + } + + // If the file is too large then exit + if ($curl_info["download_content_length"] > 1000000) { + return($siteinfo); + } + + // If it isn't a HTML file then exit + if (($curl_info["content_type"] != "") && !strstr(strtolower($curl_info["content_type"]), "html")) { + return($siteinfo); + } + + if ($do_oembed) { + + $oembed_data = oembed_fetch_url($url); + + if (!in_array($oembed_data->type, array("error", "rich"))) { + $siteinfo["type"] = $oembed_data->type; + } + + if (($oembed_data->type == "link") && ($siteinfo["type"] != "photo")) { + if (isset($oembed_data->title)) { + $siteinfo["title"] = $oembed_data->title; + } + if (isset($oembed_data->description)) { + $siteinfo["text"] = trim($oembed_data->description); + } + if (isset($oembed_data->thumbnail_url)) { + $siteinfo["image"] = $oembed_data->thumbnail_url; + } + } + } + + // Fetch the first mentioned charset. Can be in body or header + $charset = ""; + if (preg_match('/charset=(.*?)['."'".'"\s\n]/', $header, $matches)) { + $charset = trim(trim(trim(array_pop($matches)), ';,')); + } + + if ($charset == "") { + $charset = "utf-8"; + } + + $pos = strpos($header, "\r\n\r\n"); + + if ($pos) { + $body = trim(substr($header, $pos)); + } else { + $body = $header; + } + + if (($charset != "") && (strtoupper($charset) != "UTF-8")) { + logger("parseurl_getsiteinfo: detected charset ".$charset, LOGGER_DEBUG); + //$body = mb_convert_encoding($body, "UTF-8", $charset); + $body = iconv($charset, "UTF-8//TRANSLIT", $body); + } + + $body = mb_convert_encoding($body, 'HTML-ENTITIES', "UTF-8"); + + $doc = new \DOMDocument(); + @$doc->loadHTML($body); + + \xml::deleteNode($doc, "style"); + \xml::deleteNode($doc, "script"); + \xml::deleteNode($doc, "option"); + \xml::deleteNode($doc, "h1"); + \xml::deleteNode($doc, "h2"); + \xml::deleteNode($doc, "h3"); + \xml::deleteNode($doc, "h4"); + \xml::deleteNode($doc, "h5"); + \xml::deleteNode($doc, "h6"); + \xml::deleteNode($doc, "ol"); + \xml::deleteNode($doc, "ul"); + + $xpath = new \DomXPath($doc); + + $list = $xpath->query("//meta[@content]"); + foreach ($list as $node) { + $attr = array(); + if ($node->attributes->length) { + foreach ($node->attributes as $attribute) { + $attr[$attribute->name] = $attribute->value; + } + } + + if (@$attr["http-equiv"] == "refresh") { + $path = $attr["content"]; + $pathinfo = explode(";", $path); + $content = ""; + foreach ($pathinfo as $value) { + if (substr(strtolower($value), 0, 4) == "url=") { + $content = substr($value, 4); + } + } + if ($content != "") { + $siteinfo = self::getSiteinfo($content, $no_guessing, $do_oembed, ++$count); + return($siteinfo); + } + } + } + + $list = $xpath->query("//title"); + if ($list->length > 0) { + $siteinfo["title"] = $list->item(0)->nodeValue; + } + + //$list = $xpath->query("head/meta[@name]"); + $list = $xpath->query("//meta[@name]"); + foreach ($list as $node) { + $attr = array(); + if ($node->attributes->length) { + foreach ($node->attributes as $attribute) { + $attr[$attribute->name] = $attribute->value; + } + } + + $attr["content"] = trim(html_entity_decode($attr["content"], ENT_QUOTES, "UTF-8")); + + if ($attr["content"] != "") { + switch (strtolower($attr["name"])) { + case "fulltitle": + $siteinfo["title"] = $attr["content"]; + break; + case "description": + $siteinfo["text"] = $attr["content"]; + break; + case "thumbnail": + $siteinfo["image"] = $attr["content"]; + break; + case "twitter:image": + $siteinfo["image"] = $attr["content"]; + break; + case "twitter:image:src": + $siteinfo["image"] = $attr["content"]; + break; + case "twitter:card": + if (($siteinfo["type"] == "") || ($attr["content"] == "photo")) { + $siteinfo["type"] = $attr["content"]; + } + break; + case "twitter:description": + $siteinfo["text"] = $attr["content"]; + break; + case "twitter:title": + $siteinfo["title"] = $attr["content"]; + break; + case "dc.title": + $siteinfo["title"] = $attr["content"]; + break; + case "dc.description": + $siteinfo["text"] = $attr["content"]; + break; + case "keywords": + $keywords = explode(",", $attr["content"]); + break; + case "news_keywords": + $keywords = explode(",", $attr["content"]); + break; + } + } + if ($siteinfo["type"] == "summary") { + $siteinfo["type"] = "link"; + } + } + + if (isset($keywords)) { + $siteinfo["keywords"] = array(); + foreach ($keywords as $keyword) { + if (!in_array(trim($keyword), $siteinfo["keywords"])) { + $siteinfo["keywords"][] = trim($keyword); + } + } + } + + //$list = $xpath->query("head/meta[@property]"); + $list = $xpath->query("//meta[@property]"); + foreach ($list as $node) { + $attr = array(); + if ($node->attributes->length) { + foreach ($node->attributes as $attribute) { + $attr[$attribute->name] = $attribute->value; + } + } + + $attr["content"] = trim(html_entity_decode($attr["content"], ENT_QUOTES, "UTF-8")); + + if ($attr["content"] != "") { + switch (strtolower($attr["property"])) { + case "og:image": + $siteinfo["image"] = $attr["content"]; + break; + case "og:title": + $siteinfo["title"] = $attr["content"]; + break; + case "og:description": + $siteinfo["text"] = $attr["content"]; + break; + } + } + } + + if ((@$siteinfo["image"] == "") && !$no_guessing) { + $list = $xpath->query("//img[@src]"); + foreach ($list as $node) { + $attr = array(); + if ($node->attributes->length) { + foreach ($node->attributes as $attribute) { + $attr[$attribute->name] = $attribute->value; + } + } + + $src = self::completeUrl($attr["src"], $url); + $photodata = get_photo_info($src); + + if (($photodata) && ($photodata[0] > 150) && ($photodata[1] > 150)) { + if ($photodata[0] > 300) { + $photodata[1] = round($photodata[1] * (300 / $photodata[0])); + $photodata[0] = 300; + } + if ($photodata[1] > 300) { + $photodata[0] = round($photodata[0] * (300 / $photodata[1])); + $photodata[1] = 300; + } + $siteinfo["images"][] = array("src" => $src, + "width" => $photodata[0], + "height" => $photodata[1]); + } + + } + } elseif ($siteinfo["image"] != "") { + $src = self::completeUrl($siteinfo["image"], $url); + + unset($siteinfo["image"]); + + $photodata = get_photo_info($src); + + if (($photodata) && ($photodata[0] > 10) && ($photodata[1] > 10)) { + $siteinfo["images"][] = array("src" => $src, + "width" => $photodata[0], + "height" => $photodata[1]); + } + } + + if ((@$siteinfo["text"] == "") && (@$siteinfo["title"] != "") && !$no_guessing) { + $text = ""; + + $list = $xpath->query("//div[@class='article']"); + foreach ($list as $node) { + if (strlen($node->nodeValue) > 40) { + $text .= " ".trim($node->nodeValue); + } + } + + if ($text == "") { + $list = $xpath->query("//div[@class='content']"); + foreach ($list as $node) { + if (strlen($node->nodeValue) > 40) { + $text .= " ".trim($node->nodeValue); + } + } + } + + // If none text was found then take the paragraph content + if ($text == "") { + $list = $xpath->query("//p"); + foreach ($list as $node) { + if (strlen($node->nodeValue) > 40) { + $text .= " ".trim($node->nodeValue); + } + } + } + + if ($text != "") { + $text = trim(str_replace(array("\n", "\r"), array(" ", " "), $text)); + + while (strpos($text, " ")) { + $text = trim(str_replace(" ", " ", $text)); + } + + $siteinfo["text"] = trim(html_entity_decode(substr($text, 0, 350), ENT_QUOTES, "UTF-8").'...'); + } + } + + logger("parseurl_getsiteinfo: Siteinfo for ".$url." ".print_r($siteinfo, true), LOGGER_DEBUG); + + call_hooks("getsiteinfo", $siteinfo); + + return($siteinfo); + } + + /** + * @brief Convert tags from CSV to an array + * + * @param string $string Tags + * @return array with formatted Hashtags + */ + public static function convertTagsToArray($string) { + $arr_tags = str_getcsv($string); + if (count($arr_tags)) { + // add the # sign to every tag + array_walk($arr_tags, array("self", "arrAddHashes")); + + return $arr_tags; + } + } + + /** + * @brief Add a hasht sign to a string + * + * This method is used as callback function + * + * @param string $tag The pure tag name + * @param int $k Counter for internal use + */ + private static function arrAddHashes(&$tag, $k) { + $tag = "#" . $tag; + } + + /** + * @brief Add a scheme to an url + * + * The src attribute of some html elements (e.g. images) + * can miss the scheme so we need to add the correct + * scheme + * + * @param string $url The url which possibly does have + * a missing scheme (a link to an image) + * @param string $scheme The url with a correct scheme + * (e.g. the url from the webpage which does contain the image) + * + * @return string The url with a scheme + */ + private static function completeUrl($url, $scheme) { + $urlarr = parse_url($url); + + // If the url does allready have an scheme + // we can stop the process here + if (isset($urlarr["scheme"])) { + return($url); + } + + $schemearr = parse_url($scheme); + + $complete = $schemearr["scheme"]."://".$schemearr["host"]; + + if (@$schemearr["port"] != "") { + $complete .= ":".$schemearr["port"]; + } + + if (strpos($urlarr["path"],"/") !== 0) { + $complete .= "/"; + } + + $complete .= $urlarr["path"]; + + if (@$urlarr["query"] != "") { + $complete .= "?".$urlarr["query"]; + } + + if (@$urlarr["fragment"] != "") { + $complete .= "#".$urlarr["fragment"]; + } + + return($complete); + } +} diff --git a/include/Photo.php b/include/Photo.php index 9732801c9a..828dce82d7 100644 --- a/include/Photo.php +++ b/include/Photo.php @@ -1,133 +1,141 @@ 'jpg', - 'image/png' => 'png', - 'image/gif' => 'gif' - ); - } else { - $t = array(); - $t['image/jpeg'] ='jpg'; - if (imagetypes() & IMG_PNG) $t['image/png'] = 'png'; + /** + * @brief supported mimetypes and corresponding file extensions + */ + static function supportedTypes() { + if (class_exists('Imagick')) { + + // Imagick::queryFormats won't help us a lot there... + // At least, not yet, other parts of friendica uses this array + $t = array( + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif' + ); + } else { + $t = array(); + $t['image/jpeg'] ='jpg'; + if (imagetypes() & IMG_PNG) { + $t['image/png'] = 'png'; + } + } + + return $t; } - return $t; - } + public function __construct($data, $type=null) { + $this->imagick = class_exists('Imagick'); + $this->types = $this->supportedTypes(); + if (!array_key_exists($type, $this->types)){ + $type='image/jpeg'; + } + $this->type = $type; - public function __construct($data, $type=null) { - $this->imagick = class_exists('Imagick'); - $this->types = $this->supportedTypes(); - if (!array_key_exists($type,$this->types)){ - $type='image/jpeg'; - } - $this->type = $type; - - if($this->is_imagick() && $this->load_data($data)) { + if ($this->is_imagick() && $this->load_data($data)) { return true; } else { // Failed to load with Imagick, fallback $this->imagick = false; } return $this->load_data($data); - } - - public function __destruct() { - if($this->image) { - if($this->is_imagick()) { - $this->image->clear(); - $this->image->destroy(); - return; - } - imagedestroy($this->image); } - } - public function is_imagick() { - return $this->imagick; - } - - /** - * Maps Mime types to Imagick formats - */ - public function get_FormatsMap() { - $m = array( - 'image/jpeg' => 'JPG', - 'image/png' => 'PNG', - 'image/gif' => 'GIF' - ); - return $m; - } - - private function load_data($data) { - if($this->is_imagick()) { - $this->image = new Imagick(); - try { - $this->image->readImageBlob($data); + public function __destruct() { + if ($this->image) { + if ($this->is_imagick()) { + $this->image->clear(); + $this->image->destroy(); + return; } - catch (Exception $e) { + imagedestroy($this->image); + } + } + + public function is_imagick() { + return $this->imagick; + } + + /** + * @brief Maps Mime types to Imagick formats + * @return arr With with image formats (mime type as key) + */ + public function get_FormatsMap() { + $m = array( + 'image/jpeg' => 'JPG', + 'image/png' => 'PNG', + 'image/gif' => 'GIF' + ); + return $m; + } + + private function load_data($data) { + if ($this->is_imagick()) { + $this->image = new Imagick(); + try { + $this->image->readImageBlob($data); + } catch (Exception $e) { // Imagick couldn't use the data return false; } - /** - * Setup the image to the format it will be saved to - */ - $map = $this->get_FormatsMap(); - $format = $map[$type]; - $this->image->setFormat($format); + /* + * Setup the image to the format it will be saved to + */ + $map = $this->get_FormatsMap(); + $format = $map[$type]; + $this->image->setFormat($format); - // Always coalesce, if it is not a multi-frame image it won't hurt anyway - $this->image = $this->image->coalesceImages(); + // Always coalesce, if it is not a multi-frame image it won't hurt anyway + $this->image = $this->image->coalesceImages(); - /** - * setup the compression here, so we'll do it only once - */ - switch($this->getType()){ - case "image/png": - $quality = get_config('system','png_quality'); - if((! $quality) || ($quality > 9)) - $quality = PNG_QUALITY; - /** - * From http://www.imagemagick.org/script/command-line-options.php#quality: - * - * 'For the MNG and PNG image formats, the quality value sets - * the zlib compression level (quality / 10) and filter-type (quality % 10). - * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering, - * unless the image has a color map, in which case it means compression level 7 with no PNG filtering' - */ - $quality = $quality * 10; - $this->image->setCompressionQuality($quality); - break; - case "image/jpeg": - $quality = get_config('system','jpeg_quality'); - if((! $quality) || ($quality > 100)) - $quality = JPEG_QUALITY; - $this->image->setCompressionQuality($quality); - } + /* + * setup the compression here, so we'll do it only once + */ + switch($this->getType()){ + case "image/png": + $quality = get_config('system', 'png_quality'); + if ((! $quality) || ($quality > 9)) { + $quality = PNG_QUALITY; + } + /* + * From http://www.imagemagick.org/script/command-line-options.php#quality: + * + * 'For the MNG and PNG image formats, the quality value sets + * the zlib compression level (quality / 10) and filter-type (quality % 10). + * The default PNG "quality" is 75, which means compression level 7 with adaptive PNG filtering, + * unless the image has a color map, in which case it means compression level 7 with no PNG filtering' + */ + $quality = $quality * 10; + $this->image->setCompressionQuality($quality); + break; + case "image/jpeg": + $quality = get_config('system', 'jpeg_quality'); + if ((! $quality) || ($quality > 100)) { + $quality = JPEG_QUALITY; + } + $this->image->setCompressionQuality($quality); + } // The 'width' and 'height' properties are only used by non-Imagick routines. $this->width = $this->image->getImageWidth(); @@ -139,7 +147,7 @@ class Photo { $this->valid = false; $this->image = @imagecreatefromstring($data); - if($this->image !== FALSE) { + if ($this->image !== false) { $this->width = imagesx($this->image); $this->height = imagesy($this->image); $this->valid = true; @@ -148,123 +156,125 @@ class Photo { return true; } - + return false; } - public function is_valid() { - if($this->is_imagick()) - return ($this->image !== FALSE); - return $this->valid; - } - - public function getWidth() { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) - return $this->image->getImageWidth(); - return $this->width; - } - - public function getHeight() { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) - return $this->image->getImageHeight(); - return $this->height; - } - - public function getImage() { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) { - /* Clean it */ - $this->image = $this->image->deconstructImages(); - return $this->image; + public function is_valid() { + if ($this->is_imagick()) { + return ($this->image !== false); + } + return $this->valid; } - return $this->image; - } - public function getType() { - if(!$this->is_valid()) - return FALSE; + public function getWidth() { + if (!$this->is_valid()) { + return false; + } - return $this->type; - } + if ($this->is_imagick()) { + return $this->image->getImageWidth(); + } + return $this->width; + } - public function getExt() { - if(!$this->is_valid()) - return FALSE; + public function getHeight() { + if (!$this->is_valid()) { + return false; + } - return $this->types[$this->getType()]; - } + if ($this->is_imagick()) { + return $this->image->getImageHeight(); + } + return $this->height; + } - public function scaleImage($max) { - if(!$this->is_valid()) - return FALSE; + public function getImage() { + if (!$this->is_valid()) { + return false; + } - $width = $this->getWidth(); - $height = $this->getHeight(); + if ($this->is_imagick()) { + /* Clean it */ + $this->image = $this->image->deconstructImages(); + return $this->image; + } + return $this->image; + } - $dest_width = $dest_height = 0; + public function getType() { + if (!$this->is_valid()) { + return false; + } - if((! $width)|| (! $height)) - return FALSE; + return $this->type; + } - if($width > $max && $height > $max) { + public function getExt() { + if (!$this->is_valid()) { + return false; + } + + return $this->types[$this->getType()]; + } + + public function scaleImage($max) { + if (!$this->is_valid()) { + return false; + } + + $width = $this->getWidth(); + $height = $this->getHeight(); + + $dest_width = $dest_height = 0; + + if ((! $width)|| (! $height)) { + return false; + } + + if ($width > $max && $height > $max) { // very tall image (greater than 16:9) // constrain the width - let the height float. - if((($height * 9) / 16) > $width) { + if ((($height * 9) / 16) > $width) { $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); + $dest_height = intval(($height * $max) / $width); + } elseif ($width > $height) { + // else constrain both dimensions + $dest_width = $max; + $dest_height = intval(($height * $max) / $width); + } else { + $dest_width = intval(($width * $max) / $height); + $dest_height = $max; } - - // else constrain both dimensions - - elseif($width > $height) { - $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } - else { - $dest_width = intval(( $width * $max ) / $height); - $dest_height = $max; - } - } - else { - if( $width > $max ) { - $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } - else { - if( $height > $max ) { + } else { + if ($width > $max) { + $dest_width = $max; + $dest_height = intval(($height * $max) / $width); + } else { + if ($height > $max) { // very tall image (greater than 16:9) // but width is OK - don't do anything - if((($height * 9) / 16) > $width) { + if ((($height * 9) / 16) > $width) { $dest_width = $width; - $dest_height = $height; - } - else { - $dest_width = intval(( $width * $max ) / $height); - $dest_height = $max; + $dest_height = $height; + } else { + $dest_width = intval(($width * $max) / $height); + $dest_height = $max; } + } else { + $dest_width = $width; + $dest_height = $height; + } + } } - else { - $dest_width = $width; - $dest_height = $height; - } - } - } - if($this->is_imagick()) { - /** + if ($this->is_imagick()) { + /* * If it is not animated, there will be only one iteration here, * so don't bother checking */ @@ -273,7 +283,7 @@ class Photo { do { // FIXME - implement horizantal bias for scaling as in followin GD functions - // to allow very tall images to be constrained only horizontally. + // to allow very tall images to be constrained only horizontally. $this->image->scaleImage($dest_width, $dest_height); } while ($this->image->nextImage()); @@ -283,234 +293,253 @@ class Photo { $this->height = $this->image->getImageHeight(); return; - } - - - $dest = imagecreatetruecolor( $dest_width, $dest_height ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } - - public function rotate($degrees) { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) { - $this->image->setFirstIterator(); - do { - $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate() - } while ($this->image->nextImage()); - return; - } - - $this->image = imagerotate($this->image,$degrees,0); - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } - - public function flip($horiz = true, $vert = false) { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) { - $this->image->setFirstIterator(); - do { - if($horiz) $this->image->flipImage(); - if($vert) $this->image->flopImage(); - } while ($this->image->nextImage()); - return; - } - - $w = imagesx($this->image); - $h = imagesy($this->image); - $flipped = imagecreate($w, $h); - if($horiz) { - for ($x = 0; $x < $w; $x++) { - imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h); - } - } - if($vert) { - for ($y = 0; $y < $h; $y++) { - imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1); - } - } - $this->image = $flipped; - } - - public function orient($filename) { - if ($this->is_imagick()) { - // based off comment on http://php.net/manual/en/imagick.getimageorientation.php - $orientation = $this->image->getImageOrientation(); - switch ($orientation) { - case imagick::ORIENTATION_BOTTOMRIGHT: - $this->image->rotateimage("#000", 180); - break; - case imagick::ORIENTATION_RIGHTTOP: - $this->image->rotateimage("#000", 90); - break; - case imagick::ORIENTATION_LEFTBOTTOM: - $this->image->rotateimage("#000", -90); - break; } - $this->image->setImageOrientation(imagick::ORIENTATION_TOPLEFT); - return TRUE; - } - // based off comment on http://php.net/manual/en/function.imagerotate.php - if(!$this->is_valid()) - return FALSE; - - if( (! function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg') ) - return; - - $exif = @exif_read_data($filename,null,true); - if(! $exif) - return; - - $ort = $exif['IFD0']['Orientation']; - - switch($ort) - { - case 1: // nothing - break; - - case 2: // horizontal flip - $this->flip(); - break; - - case 3: // 180 rotate left - $this->rotate(180); - break; - - case 4: // vertical flip - $this->flip(false, true); - break; - - case 5: // vertical flip + 90 rotate right - $this->flip(false, true); - $this->rotate(-90); - break; - - case 6: // 90 rotate right - $this->rotate(-90); - break; - - case 7: // horizontal flip + 90 rotate right - $this->flip(); - $this->rotate(-90); - break; - - case 8: // 90 rotate left - $this->rotate(90); - break; - } - - // logger('exif: ' . print_r($exif,true)); - return $exif; - - } - - - - public function scaleImageUp($min) { - if(!$this->is_valid()) - return FALSE; - - - $width = $this->getWidth(); - $height = $this->getHeight(); - - $dest_width = $dest_height = 0; - - if((! $width)|| (! $height)) - return FALSE; - - if($width < $min && $height < $min) { - if($width > $height) { - $dest_width = $min; - $dest_height = intval(( $height * $min ) / $width); - } - else { - $dest_width = intval(( $width * $min ) / $height); - $dest_height = $min; - } - } - else { - if( $width < $min ) { - $dest_width = $min; - $dest_height = intval(( $height * $min ) / $width); - } - else { - if( $height < $min ) { - $dest_width = intval(( $width * $min ) / $height); - $dest_height = $min; + $dest = imagecreatetruecolor($dest_width, $dest_height); + imagealphablending($dest, false); + imagesavealpha($dest, true); + if ($this->type=='image/png') { + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha } - else { - $dest_width = $width; - $dest_height = $height; + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); + if ($this->image) { + imagedestroy($this->image); } - } + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); } - if($this->is_imagick()) - return $this->scaleImage($dest_width,$dest_height); + public function rotate($degrees) { + if (!$this->is_valid()) { + return false; + } - $dest = imagecreatetruecolor( $dest_width, $dest_height ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } + if ($this->is_imagick()) { + $this->image->setFirstIterator(); + do { + $this->image->rotateImage(new ImagickPixel(), -$degrees); // ImageMagick rotates in the opposite direction of imagerotate() + } while ($this->image->nextImage()); + return; + } - - - public function scaleImageSquare($dim) { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) { - $this->image->setFirstIterator(); - do { - $this->image->scaleImage($dim, $dim); - } while ($this->image->nextImage()); - return; + $this->image = imagerotate($this->image,$degrees,0); + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); } - $dest = imagecreatetruecolor( $dim, $dim ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } + public function flip($horiz = true, $vert = false) { + if (!$this->is_valid()) { + return false; + } + + if ($this->is_imagick()) { + $this->image->setFirstIterator(); + do { + if ($horiz) { + $this->image->flipImage(); + } + if ($vert) { + $this->image->flopImage(); + } + } while ($this->image->nextImage()); + return; + } + + $w = imagesx($this->image); + $h = imagesy($this->image); + $flipped = imagecreate($w, $h); + if ($horiz) { + for ($x = 0; $x < $w; $x++) { + imagecopy($flipped, $this->image, $x, 0, $w - $x - 1, 0, 1, $h); + } + } + if ($vert) { + for ($y = 0; $y < $h; $y++) { + imagecopy($flipped, $this->image, 0, $y, 0, $h - $y - 1, $w, 1); + } + } + $this->image = $flipped; + } + + public function orient($filename) { + if ($this->is_imagick()) { + // based off comment on http://php.net/manual/en/imagick.getimageorientation.php + $orientation = $this->image->getImageOrientation(); + switch ($orientation) { + case imagick::ORIENTATION_BOTTOMRIGHT: + $this->image->rotateimage("#000", 180); + break; + case imagick::ORIENTATION_RIGHTTOP: + $this->image->rotateimage("#000", 90); + break; + case imagick::ORIENTATION_LEFTBOTTOM: + $this->image->rotateimage("#000", -90); + break; + } + + $this->image->setImageOrientation(imagick::ORIENTATION_TOPLEFT); + return true; + } + // based off comment on http://php.net/manual/en/function.imagerotate.php + + if (!$this->is_valid()) { + return false; + } + + if ((!function_exists('exif_read_data')) || ($this->getType() !== 'image/jpeg')) { + return; + } + + $exif = @exif_read_data($filename,null,true); + if (!$exif) { + return; + } + + $ort = $exif['IFD0']['Orientation']; + + switch($ort) + { + case 1: // nothing + break; + + case 2: // horizontal flip + $this->flip(); + break; + + case 3: // 180 rotate left + $this->rotate(180); + break; + + case 4: // vertical flip + $this->flip(false, true); + break; + + case 5: // vertical flip + 90 rotate right + $this->flip(false, true); + $this->rotate(-90); + break; + + case 6: // 90 rotate right + $this->rotate(-90); + break; + + case 7: // horizontal flip + 90 rotate right + $this->flip(); + $this->rotate(-90); + break; + + case 8: // 90 rotate left + $this->rotate(90); + break; + } + + // logger('exif: ' . print_r($exif,true)); + return $exif; + + } - public function cropImage($max,$x,$y,$w,$h) { - if(!$this->is_valid()) - return FALSE; - if($this->is_imagick()) { + public function scaleImageUp($min) { + if (!$this->is_valid()) { + return false; + } + + + $width = $this->getWidth(); + $height = $this->getHeight(); + + $dest_width = $dest_height = 0; + + if ((!$width)|| (!$height)) { + return false; + } + + if ($width < $min && $height < $min) { + if ($width > $height) { + $dest_width = $min; + $dest_height = intval(($height * $min) / $width); + } else { + $dest_width = intval(($width * $min) / $height); + $dest_height = $min; + } + } else { + if ($width < $min) { + $dest_width = $min; + $dest_height = intval(($height * $min) / $width); + } else { + if ($height < $min) { + $dest_width = intval(($width * $min) / $height); + $dest_height = $min; + } else { + $dest_width = $width; + $dest_height = $height; + } + } + } + + if ($this->is_imagick()) { + return $this->scaleImage($dest_width, $dest_height); + } + + $dest = imagecreatetruecolor($dest_width, $dest_height); + imagealphablending($dest, false); + imagesavealpha($dest, true); + if ($this->type=='image/png') { + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha + } + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dest_width, $dest_height, $width, $height); + if ($this->image) { + imagedestroy($this->image); + } + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + + + public function scaleImageSquare($dim) { + if (!$this->is_valid()) { + return false; + } + + if ($this->is_imagick()) { + $this->image->setFirstIterator(); + do { + $this->image->scaleImage($dim, $dim); + } while ($this->image->nextImage()); + return; + } + + $dest = imagecreatetruecolor($dim, $dim); + imagealphablending($dest, false); + imagesavealpha($dest, true); + if ($this->type=='image/png') { + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha + } + imagecopyresampled($dest, $this->image, 0, 0, 0, 0, $dim, $dim, $this->width, $this->height); + if ($this->image) { + imagedestroy($this->image); + } + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); + } + + + public function cropImage($max, $x, $y, $w, $h) { + if (!$this->is_valid()) { + return false; + } + + if ($this->is_imagick()) { $this->image->setFirstIterator(); do { $this->image->cropImage($w, $h, $x, $y); - /** + /* * We need to remove the canva, * or the image is not resized to the crop: * http://php.net/manual/en/imagick.cropimage.php#97232 @@ -520,159 +549,167 @@ class Photo { return $this->scaleImage($max); } - $dest = imagecreatetruecolor( $max, $max ); - imagealphablending($dest, false); - imagesavealpha($dest, true); - if ($this->type=='image/png') imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha - imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h); - if($this->image) - imagedestroy($this->image); - $this->image = $dest; - $this->width = imagesx($this->image); - $this->height = imagesy($this->image); - } - - public function saveImage($path) { - if(!$this->is_valid()) - return FALSE; - - $string = $this->imageString(); - - $a = get_app(); - - $stamp1 = microtime(true); - file_put_contents($path, $string); - $a->save_timestamp($stamp1, "file"); - } - - public function imageString() { - if(!$this->is_valid()) - return FALSE; - - if($this->is_imagick()) { - /* Clean it */ - $this->image = $this->image->deconstructImages(); - $string = $this->image->getImagesBlob(); - return $string; + $dest = imagecreatetruecolor($max, $max); + imagealphablending($dest, false); + imagesavealpha($dest, true); + if ($this->type=='image/png') { + imagefill($dest, 0, 0, imagecolorallocatealpha($dest, 0, 0, 0, 127)); // fill with alpha + } + imagecopyresampled($dest, $this->image, 0, 0, $x, $y, $max, $max, $w, $h); + if ($this->image) { + imagedestroy($this->image); + } + $this->image = $dest; + $this->width = imagesx($this->image); + $this->height = imagesy($this->image); } - $quality = FALSE; + public function saveImage($path) { + if (!$this->is_valid()) { + return false; + } - ob_start(); + $string = $this->imageString(); - // Enable interlacing - imageinterlace($this->image, true); + $a = get_app(); - switch($this->getType()){ - case "image/png": - $quality = get_config('system','png_quality'); - if((! $quality) || ($quality > 9)) - $quality = PNG_QUALITY; - imagepng($this->image,NULL, $quality); - break; - case "image/jpeg": - $quality = get_config('system','jpeg_quality'); - if((! $quality) || ($quality > 100)) - $quality = JPEG_QUALITY; - imagejpeg($this->image,NULL,$quality); + $stamp1 = microtime(true); + file_put_contents($path, $string); + $a->save_timestamp($stamp1, "file"); } - $string = ob_get_contents(); - ob_end_clean(); - return $string; - } + public function imageString() { + if (!$this->is_valid()) { + return false; + } + + if ($this->is_imagick()) { + /* Clean it */ + $this->image = $this->image->deconstructImages(); + $string = $this->image->getImagesBlob(); + return $string; + } + + $quality = false; + + ob_start(); + + // Enable interlacing + imageinterlace($this->image, true); + + switch($this->getType()){ + case "image/png": + $quality = get_config('system', 'png_quality'); + if ((!$quality) || ($quality > 9)) { + $quality = PNG_QUALITY; + } + imagepng($this->image, null, $quality); + break; + case "image/jpeg": + $quality = get_config('system', 'jpeg_quality'); + if ((!$quality) || ($quality > 100)) { + $quality = JPEG_QUALITY; + } + imagejpeg($this->image, null, $quality); + } + $string = ob_get_contents(); + ob_end_clean(); + + return $string; + } - public function store($uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '') { + public function store($uid, $cid, $rid, $filename, $album, $scale, $profile = 0, $allow_cid = '', $allow_gid = '', $deny_cid = '', $deny_gid = '') { - $r = q("select `guid` from photo where `resource-id` = '%s' and `guid` != '' limit 1", - dbesc($rid) - ); - if(count($r)) - $guid = $r[0]['guid']; - else - $guid = get_guid(); + $r = q("SELECT `guid` FROM `photo` WHERE `resource-id` = '%s' AND `guid` != '' LIMIT 1", + dbesc($rid) + ); + if (dbm::is_result($r)) { + $guid = $r[0]['guid']; + } else { + $guid = get_guid(); + } - $x = q("select id from photo where `resource-id` = '%s' and uid = %d and `contact-id` = %d and `scale` = %d limit 1", - dbesc($rid), - intval($uid), - intval($cid), - intval($scale) - ); - if(count($x)) { - $r = q("UPDATE `photo` - set `uid` = %d, - `contact-id` = %d, - `guid` = '%s', - `resource-id` = '%s', - `created` = '%s', - `edited` = '%s', - `filename` = '%s', - `type` = '%s', - `album` = '%s', - `height` = %d, - `width` = %d, + $x = q("SELECT `id` FROM `photo` WHERE `resource-id` = '%s' AND `uid` = %d AND `contact-id` = %d AND `scale` = %d LIMIT 1", + dbesc($rid), + intval($uid), + intval($cid), + intval($scale) + ); + if (dbm::is_result($x)) { + $r = q("UPDATE `photo` + SET `uid` = %d, + `contact-id` = %d, + `guid` = '%s', + `resource-id` = '%s', + `created` = '%s', + `edited` = '%s', + `filename` = '%s', + `type` = '%s', + `album` = '%s', + `height` = %d, + `width` = %d, `datasize` = %d, - `data` = '%s', - `scale` = %d, - `profile` = %d, - `allow_cid` = '%s', - `allow_gid` = '%s', - `deny_cid` = '%s', - `deny_gid` = '%s' - where id = %d", + `data` = '%s', + `scale` = %d, + `profile` = %d, + `allow_cid` = '%s', + `allow_gid` = '%s', + `deny_cid` = '%s', + `deny_gid` = '%s' + WHERE `id` = %d", - intval($uid), - intval($cid), - dbesc($guid), - dbesc($rid), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - dbesc(basename($filename)), - dbesc($this->getType()), - dbesc($album), - intval($this->getHeight()), - intval($this->getWidth()), + intval($uid), + intval($cid), + dbesc($guid), + dbesc($rid), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(basename($filename)), + dbesc($this->getType()), + dbesc($album), + intval($this->getHeight()), + intval($this->getWidth()), dbesc(strlen($this->imageString())), - dbesc($this->imageString()), - intval($scale), - intval($profile), - dbesc($allow_cid), - dbesc($allow_gid), - dbesc($deny_cid), - dbesc($deny_gid), - intval($x[0]['id']) - ); - } - else { - $r = q("INSERT INTO `photo` - ( `uid`, `contact-id`, `guid`, `resource-id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `datasize`, `data`, `scale`, `profile`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid` ) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', %d, %d, '%s', '%s', '%s', '%s' )", - intval($uid), - intval($cid), - dbesc($guid), - dbesc($rid), - dbesc(datetime_convert()), - dbesc(datetime_convert()), - dbesc(basename($filename)), - dbesc($this->getType()), - dbesc($album), - intval($this->getHeight()), - intval($this->getWidth()), + dbesc($this->imageString()), + intval($scale), + intval($profile), + dbesc($allow_cid), + dbesc($allow_gid), + dbesc($deny_cid), + dbesc($deny_gid), + intval($x[0]['id']) + ); + } else { + $r = q("INSERT INTO `photo` + (`uid`, `contact-id`, `guid`, `resource-id`, `created`, `edited`, `filename`, type, `album`, `height`, `width`, `datasize`, `data`, `scale`, `profile`, `allow_cid`, `allow_gid`, `deny_cid`, `deny_gid`) + VALUES (%d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, %d, '%s', %d, %d, '%s', '%s', '%s', '%s')", + intval($uid), + intval($cid), + dbesc($guid), + dbesc($rid), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc(basename($filename)), + dbesc($this->getType()), + dbesc($album), + intval($this->getHeight()), + intval($this->getWidth()), dbesc(strlen($this->imageString())), - dbesc($this->imageString()), - intval($scale), - intval($profile), - dbesc($allow_cid), - dbesc($allow_gid), - dbesc($deny_cid), - dbesc($deny_gid) - ); + dbesc($this->imageString()), + intval($scale), + intval($profile), + dbesc($allow_cid), + dbesc($allow_gid), + dbesc($deny_cid), + dbesc($deny_gid) + ); + } + + return $r; } - return $r; - } -}} +} /** @@ -682,103 +719,144 @@ class Photo { * @arg $fromcurl boolean Check Content-Type header from curl request */ function guess_image_type($filename, $fromcurl=false) { - logger('Photo: guess_image_type: '.$filename . ($fromcurl?' from curl headers':''), LOGGER_DEBUG); - $type = null; - if ($fromcurl) { - $a = get_app(); - $headers=array(); - $h = explode("\n",$a->get_curl_headers()); - foreach ($h as $l) { - list($k,$v) = array_map("trim", explode(":", trim($l), 2)); - $headers[$k] = $v; + logger('Photo: guess_image_type: '.$filename . ($fromcurl?' from curl headers':''), LOGGER_DEBUG); + $type = null; + if ($fromcurl) { + $a = get_app(); + $headers=array(); + $h = explode("\n",$a->get_curl_headers()); + foreach ($h as $l) { + list($k,$v) = array_map("trim", explode(":", trim($l), 2)); + $headers[$k] = $v; + } + if (array_key_exists('Content-Type', $headers)) + $type = $headers['Content-Type']; } - if (array_key_exists('Content-Type', $headers)) - $type = $headers['Content-Type']; - } - if (is_null($type)){ - // Guessing from extension? Isn't that... dangerous? - if(class_exists('Imagick') && file_exists($filename) && is_readable($filename)) { - /** - * Well, this not much better, - * but at least it comes from the data inside the image, - * we won't be tricked by a manipulated extension - */ - $image = new Imagick($filename); - $type = $image->getImageMimeType(); - $image->setInterlaceScheme(Imagick::INTERLACE_PLANE); - } else { - $ext = pathinfo($filename, PATHINFO_EXTENSION); - $types = Photo::supportedTypes(); - $type = "image/jpeg"; - foreach ($types as $m=>$e){ - if ($ext==$e) $type = $m; - } + if (is_null($type)){ + // Guessing from extension? Isn't that... dangerous? + if (class_exists('Imagick') && file_exists($filename) && is_readable($filename)) { + /** + * Well, this not much better, + * but at least it comes from the data inside the image, + * we won't be tricked by a manipulated extension + */ + $image = new Imagick($filename); + $type = $image->getImageMimeType(); + $image->setInterlaceScheme(Imagick::INTERLACE_PLANE); + } else { + $ext = pathinfo($filename, PATHINFO_EXTENSION); + $types = Photo::supportedTypes(); + $type = "image/jpeg"; + foreach ($types as $m => $e){ + if ($ext == $e) { + $type = $m; + } + } + } } - } - logger('Photo: guess_image_type: type='.$type, LOGGER_DEBUG); - return $type; + logger('Photo: guess_image_type: type='.$type, LOGGER_DEBUG); + return $type; } -function import_profile_photo($photo,$uid,$cid) { +/** + * @brief Updates the avatar links in a contact only if needed + * + * @param string $avatar Link to avatar picture + * @param int $uid User id of contact owner + * @param int $cid Contact id + * @param bool $force force picture update + * + * @return array Returns array of the different avatar sizes + */ +function update_contact_avatar($avatar, $uid, $cid, $force = false) { - $a = get_app(); + $r = q("SELECT `avatar`, `photo`, `thumb`, `micro` FROM `contact` WHERE `id` = %d LIMIT 1", intval($cid)); + if (!dbm::is_result($r)) { + return false; + } else { + $data = array($r[0]["photo"], $r[0]["thumb"], $r[0]["micro"]); + } - $r = q("select `resource-id` from photo where `uid` = %d and `contact-id` = %d and `scale` = 4 and `album` = 'Contact Photos' limit 1", - intval($uid), - intval($cid) - ); - if(count($r) && strlen($r[0]['resource-id'])) { - $hash = $r[0]['resource-id']; - } - else { - $hash = photo_new_resource(); - } + if (($r[0]["avatar"] != $avatar) OR $force) { + $photos = import_profile_photo($avatar, $uid, $cid, true); - $photo_failure = false; + if ($photos) { + q("UPDATE `contact` SET `avatar` = '%s', `photo` = '%s', `thumb` = '%s', `micro` = '%s', `avatar-date` = '%s' WHERE `id` = %d", + dbesc($avatar), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), + dbesc(datetime_convert()), intval($cid)); + return $photos; + } + } - $filename = basename($photo); - $img_str = fetch_url($photo,true); + return $data; +} - $type = guess_image_type($photo,true); - $img = new Photo($img_str, $type); - if($img->is_valid()) { +function import_profile_photo($photo, $uid, $cid, $quit_on_error = false) { - $img->scaleImageSquare(175); + $r = q("SELECT `resource-id` FROM `photo` WHERE `uid` = %d AND `contact-id` = %d AND `scale` = 4 AND `album` = 'Contact Photos' LIMIT 1", + intval($uid), + intval($cid) + ); + if (dbm::is_result($r) && strlen($r[0]['resource-id'])) { + $hash = $r[0]['resource-id']; + } else { + $hash = photo_new_resource(); + } - $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 4 ); + $photo_failure = false; - if($r === false) - $photo_failure = true; + $filename = basename($photo); + $img_str = fetch_url($photo, true); - $img->scaleImage(80); + if ($quit_on_error AND ($img_str == "")) { + return false; + } - $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 5 ); + $type = guess_image_type($photo, true); + $img = new Photo($img_str, $type); + if ($img->is_valid()) { - if($r === false) - $photo_failure = true; + $img->scaleImageSquare(175); - $img->scaleImage(48); + $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 4); - $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 6 ); + if ($r === false) + $photo_failure = true; - if($r === false) - $photo_failure = true; + $img->scaleImage(80); - $photo = $a->get_baseurl() . '/photo/' . $hash . '-4.' . $img->getExt(); - $thumb = $a->get_baseurl() . '/photo/' . $hash . '-5.' . $img->getExt(); - $micro = $a->get_baseurl() . '/photo/' . $hash . '-6.' . $img->getExt(); - } - else - $photo_failure = true; + $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 5); - if($photo_failure) { - $photo = $a->get_baseurl() . '/images/person-175.jpg'; - $thumb = $a->get_baseurl() . '/images/person-80.jpg'; - $micro = $a->get_baseurl() . '/images/person-48.jpg'; - } + if ($r === false) + $photo_failure = true; - return(array($photo,$thumb,$micro)); + $img->scaleImage(48); + + $r = $img->store($uid, $cid, $hash, $filename, 'Contact Photos', 6); + + if ($r === false) { + $photo_failure = true; + } + + $photo = App::get_baseurl() . '/photo/' . $hash . '-4.' . $img->getExt(); + $thumb = App::get_baseurl() . '/photo/' . $hash . '-5.' . $img->getExt(); + $micro = App::get_baseurl() . '/photo/' . $hash . '-6.' . $img->getExt(); + } else { + $photo_failure = true; + } + + if ($photo_failure AND $quit_on_error) { + return false; + } + + if ($photo_failure) { + $photo = App::get_baseurl() . '/images/person-175.jpg'; + $thumb = App::get_baseurl() . '/images/person-80.jpg'; + $micro = App::get_baseurl() . '/images/person-48.jpg'; + } + + return(array($photo,$thumb,$micro)); } @@ -787,27 +865,30 @@ function get_photo_info($url) { $data = Cache::get($url); - if (is_null($data)) { + if (is_null($data) OR !$data OR !is_array($data)) { $img_str = fetch_url($url, true, $redirects, 4); - $filesize = strlen($img_str); - $tempfile = tempnam(get_temppath(), "cache"); + if (function_exists("getimagesizefromstring")) { + $data = getimagesizefromstring($img_str); + } else { + $tempfile = tempnam(get_temppath(), "cache"); - $a = get_app(); - $stamp1 = microtime(true); - file_put_contents($tempfile, $img_str); - $a->save_timestamp($stamp1, "file"); + $a = get_app(); + $stamp1 = microtime(true); + file_put_contents($tempfile, $img_str); + $a->save_timestamp($stamp1, "file"); - $data = getimagesize($tempfile); - unlink($tempfile); + $data = getimagesize($tempfile); + unlink($tempfile); + } - if ($data) + if ($data) { $data["size"] = $filesize; + } - Cache::set($url, serialize($data)); - } else - $data = unserialize($data); + Cache::set($url, $data); + } return $data; } @@ -816,40 +897,41 @@ function scale_image($width, $height, $max) { $dest_width = $dest_height = 0; - if((!$width) || (!$height)) - return FALSE; + if ((!$width) || (!$height)) { + return false; + } - if($width > $max && $height > $max) { + if ($width > $max && $height > $max) { // very tall image (greater than 16:9) // constrain the width - let the height float. - if((($height * 9) / 16) > $width) { + if ((($height * 9) / 16) > $width) { $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } elseif($width > $height) { + $dest_height = intval(($height * $max) / $width); + } elseif ($width > $height) { // else constrain both dimensions $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } else { - $dest_width = intval(( $width * $max ) / $height); + $dest_height = intval(($height * $max) / $width); + } else { + $dest_width = intval(($width * $max) / $height); $dest_height = $max; } } else { - if( $width > $max ) { + if ($width > $max) { $dest_width = $max; - $dest_height = intval(( $height * $max ) / $width); - } else { - if( $height > $max ) { + $dest_height = intval(($height * $max) / $width); + } else { + if ($height > $max) { // very tall image (greater than 16:9) // but width is OK - don't do anything - if((($height * 9) / 16) > $width) { + if ((($height * 9) / 16) > $width) { $dest_width = $width; $dest_height = $height; } else { - $dest_width = intval(( $width * $max ) / $height); + $dest_width = intval(($width * $max) / $height); $dest_height = $max; } } else { @@ -861,21 +943,21 @@ function scale_image($width, $height, $max) { return array("width" => $dest_width, "height" => $dest_height); } -function store_photo($a, $uid, $imagedata = "", $url = "") { +function store_photo(App $a, $uid, $imagedata = "", $url = "") { $r = q("SELECT `user`.`nickname`, `user`.`page-flags`, `contact`.`id` FROM `user` INNER JOIN `contact` on `user`.`uid` = `contact`.`uid` - WHERE `user`.`uid` = %d AND `user`.`blocked` = 0 and `contact`.`self` = 1 LIMIT 1", + WHERE `user`.`uid` = %d AND `user`.`blocked` = 0 AND `contact`.`self` = 1 LIMIT 1", intval($uid)); - if(!count($r)) { + if (!dbm::is_result($r)) { logger("Can't detect user data for uid ".$uid, LOGGER_DEBUG); return(array()); } $page_owner_nick = $r[0]['nickname']; -// To-Do: -// $default_cid = $r[0]['id']; -// $community_page = (($r[0]['page-flags'] == PAGE_COMMUNITY) ? true : false); + /// @TODO + /// $default_cid = $r[0]['id']; + /// $community_page = (($r[0]['page-flags'] == PAGE_COMMUNITY) ? true : false); if ((strlen($imagedata) == 0) AND ($url == "")) { logger("No image data and no url provided", LOGGER_DEBUG); @@ -888,24 +970,24 @@ function store_photo($a, $uid, $imagedata = "", $url = "") { $a->save_timestamp($stamp1, "file"); } - $maximagesize = get_config('system','maximagesize'); + $maximagesize = get_config('system', 'maximagesize'); - if(($maximagesize) && (strlen($imagedata) > $maximagesize)) { + if (($maximagesize) && (strlen($imagedata) > $maximagesize)) { logger("Image exceeds size limit of ".$maximagesize, LOGGER_DEBUG); return(array()); - } + } /* - $r = q("select sum(octet_length(data)) as total from photo where uid = %d and scale = 0 and album != 'Contact Photos' ", - intval($uid) - ); + $r = q("select sum(octet_length(data)) as total from photo where uid = %d and scale = 0 and album != 'Contact Photos' ", + intval($uid) + ); - $limit = service_class_fetch($uid,'photo_upload_limit'); + $limit = service_class_fetch($uid,'photo_upload_limit'); - if(($limit !== false) && (($r[0]['total'] + strlen($imagedata)) > $limit)) { + if (($limit !== false) && (($r[0]['total'] + strlen($imagedata)) > $limit)) { logger("Image exceeds personal limit of uid ".$uid, LOGGER_DEBUG); return(array()); - } + } */ $tempfile = tempnam(get_temppath(), "cache"); @@ -924,7 +1006,7 @@ function store_photo($a, $uid, $imagedata = "", $url = "") { $ph = new Photo($imagedata, $data["mime"]); - if(!$ph->is_valid()) { + if (!$ph->is_valid()) { unlink($tempfile); logger("Picture is no valid picture", LOGGER_DEBUG); return(array()); @@ -933,11 +1015,13 @@ function store_photo($a, $uid, $imagedata = "", $url = "") { $ph->orient($tempfile); unlink($tempfile); - $max_length = get_config('system','max_image_length'); - if(! $max_length) + $max_length = get_config('system', 'max_image_length'); + if (! $max_length) { $max_length = MAX_IMAGE_LENGTH; - if($max_length > 0) + } + if ($max_length > 0) { $ph->scaleImage($max_length); + } $width = $ph->getWidth(); $height = $ph->getHeight(); @@ -949,55 +1033,61 @@ function store_photo($a, $uid, $imagedata = "", $url = "") { // Pictures are always public by now //$defperm = '<'.$default_cid.'>'; $defperm = ""; - $visitor = 0; + $visitor = 0; $r = $ph->store($uid, $visitor, $hash, $tempfile, t('Wall Photos'), 0, 0, $defperm); - if(!$r) { + if (!$r) { logger("Picture couldn't be stored", LOGGER_DEBUG); return(array()); } - $image = array("page" => $a->get_baseurl().'/photos/'.$page_owner_nick.'/image/'.$hash, - "full" => $a->get_baseurl()."/photo/{$hash}-0.".$ph->getExt()); + $image = array("page" => App::get_baseurl().'/photos/'.$page_owner_nick.'/image/'.$hash, + "full" => App::get_baseurl()."/photo/{$hash}-0.".$ph->getExt()); - if($width > 800 || $height > 800) - $image["large"] = $a->get_baseurl()."/photo/{$hash}-0.".$ph->getExt(); + if ($width > 800 || $height > 800) { + $image["large"] = App::get_baseurl()."/photo/{$hash}-0.".$ph->getExt(); + } - if($width > 640 || $height > 640) { + if ($width > 640 || $height > 640) { $ph->scaleImage(640); $r = $ph->store($uid, $visitor, $hash, $tempfile, t('Wall Photos'), 1, 0, $defperm); - if($r) - $image["medium"] = $a->get_baseurl()."/photo/{$hash}-1.".$ph->getExt(); + if ($r) { + $image["medium"] = App::get_baseurl()."/photo/{$hash}-1.".$ph->getExt(); + } } - if($width > 320 || $height > 320) { + if ($width > 320 || $height > 320) { $ph->scaleImage(320); $r = $ph->store($uid, $visitor, $hash, $tempfile, t('Wall Photos'), 2, 0, $defperm); - if($r) - $image["small"] = $a->get_baseurl()."/photo/{$hash}-2.".$ph->getExt(); + if ($r) { + $image["small"] = App::get_baseurl()."/photo/{$hash}-2.".$ph->getExt(); + } } - if($width > 160 AND $height > 160) { + if ($width > 160 AND $height > 160) { $x = 0; $y = 0; $min = $ph->getWidth(); - if ($min > 160) + if ($min > 160) { $x = ($min - 160) / 2; + } if ($ph->getHeight() < $min) { $min = $ph->getHeight(); - if ($min > 160) + if ($min > 160) { $y = ($min - 160) / 2; + } } $min = 160; $ph->cropImage(160, $x, $y, $min, $min); $r = $ph->store($uid, $visitor, $hash, $tempfile, t('Wall Photos'), 3, 0, $defperm); - if($r) - $image["thumb"] = $a->get_baseurl()."/photo/{$hash}-3.".$ph->getExt(); + if ($r) { + $image["thumb"] = App::get_baseurl()."/photo/{$hash}-3.".$ph->getExt(); + } } // Set the full image as preview image. This will be overwritten, if the picture is larger than 640. @@ -1011,9 +1101,9 @@ function store_photo($a, $uid, $imagedata = "", $url = "") { //if (isset($image["small"])) // $image["preview"] = $image["small"]; - if (isset($image["medium"])) + if (isset($image["medium"])) { $image["preview"] = $image["medium"]; + } return($image); } - diff --git a/include/Probe.php b/include/Probe.php new file mode 100644 index 0000000000..0b3c664129 --- /dev/null +++ b/include/Probe.php @@ -0,0 +1,1151 @@ + Link to LRDD endpoint + * 'lrdd-xml' => Link to LRDD endpoint in XML format + * 'lrdd-json' => Link to LRDD endpoint in JSON format + */ + private function xrd($host) { + + $ssl_url = "https://".$host."/.well-known/host-meta"; + $url = "http://".$host."/.well-known/host-meta"; + + $xrd_timeout = Config::get('system','xrd_timeout', 20); + $redirects = 0; + + $xml = fetch_url($ssl_url, false, $redirects, $xrd_timeout, "application/xrd+xml"); + $xrd = parse_xml_string($xml, false); + + if (!is_object($xrd)) { + $xml = fetch_url($url, false, $redirects, $xrd_timeout, "application/xrd+xml"); + $xrd = parse_xml_string($xml, false); + } + if (!is_object($xrd)) + return false; + + $links = xml::element_to_array($xrd); + if (!isset($links["xrd"]["link"])) + return false; + + $xrd_data = array(); + + foreach ($links["xrd"]["link"] AS $value => $link) { + if (isset($link["@attributes"])) + $attributes = $link["@attributes"]; + elseif ($value == "@attributes") + $attributes = $link; + else + continue; + + if (($attributes["rel"] == "lrdd") AND + ($attributes["type"] == "application/xrd+xml")) + $xrd_data["lrdd-xml"] = $attributes["template"]; + elseif (($attributes["rel"] == "lrdd") AND + ($attributes["type"] == "application/json")) + $xrd_data["lrdd-json"] = $attributes["template"]; + elseif ($attributes["rel"] == "lrdd") + $xrd_data["lrdd"] = $attributes["template"]; + } + return $xrd_data; + } + + /** + * @brief Perform Webfinger lookup and return DFRN data + * + * Given an email style address, perform webfinger lookup and + * return the resulting DFRN profile URL, or if no DFRN profile URL + * is located, returns an OStatus subscription template (prefixed + * with the string 'stat:' to identify it as on OStatus template). + * If this isn't an email style address just return $webbie. + * Return an empty string if email-style addresses but webfinger fails, + * or if the resultant personal XRD doesn't contain a supported + * subscription/friend-request attribute. + * + * amended 7/9/2011 to return an hcard which could save potentially loading + * a lengthy content page to scrape dfrn attributes + * + * @param string $webbie Address that should be probed + * @param string $hcard Link to the hcard - is returned by reference + * + * @return string profile link + */ + + public static function webfinger_dfrn($webbie, &$hcard) { + + $profile_link = ''; + + $links = self::lrdd($webbie); + logger('webfinger_dfrn: '.$webbie.':'.print_r($links,true), LOGGER_DATA); + if (count($links)) { + foreach ($links as $link) { + if ($link['@attributes']['rel'] === NAMESPACE_DFRN) + $profile_link = $link['@attributes']['href']; + if (($link['@attributes']['rel'] === NAMESPACE_OSTATUSSUB) AND ($profile_link == "")) + $profile_link = 'stat:'.$link['@attributes']['template']; + if ($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard') + $hcard = $link['@attributes']['href']; + } + } + return $profile_link; + } + + /** + * @brief Check an URI for LRDD data + * + * this is a replacement for the "lrdd" function in include/network.php. + * It isn't used in this class and has some redundancies in the code. + * When time comes we can check the existing calls for "lrdd" if we can rework them. + * + * @param string $uri Address that should be probed + * + * @return array uri data + */ + public static function lrdd($uri) { + + $lrdd = self::xrd($uri); + + if (!$lrdd) { + $parts = @parse_url($uri); + if (!$parts) + return array(); + + $host = $parts["host"]; + + $path_parts = explode("/", trim($parts["path"], "/")); + + do { + $lrdd = self::xrd($host); + $host .= "/".array_shift($path_parts); + } while (!$lrdd AND (sizeof($path_parts) > 0)); + } + + if (!$lrdd) + return array(); + + foreach ($lrdd AS $key => $link) { + if ($webfinger) + continue; + + if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json"))) + continue; + + $path = str_replace('{uri}', urlencode($uri), $link); + $webfinger = self::webfinger($path); + + if (!$webfinger AND (strstr($uri, "@"))) { + $path = str_replace('{uri}', urlencode("acct:".$uri), $link); + $webfinger = self::webfinger($path); + } + } + + if (!is_array($webfinger["links"])) + return false; + + $data = array(); + + foreach ($webfinger["links"] AS $link) + $data[] = array("@attributes" => $link); + + if (is_array($webfinger["aliases"])) + foreach ($webfinger["aliases"] AS $alias) + $data[] = array("@attributes" => + array("rel" => "alias", + "href" => $alias)); + + return $data; + } + + /** + * @brief Fetch information (protocol endpoints and user information) about a given uri + * + * @param string $uri Address that should be probed + * @param string $network Test for this specific network + * @param integer $uid User ID for the probe (only used for mails) + * @param boolean $cache Use cached values? + * + * @return array uri data + */ + public static function uri($uri, $network = "", $uid = 0, $cache = true) { + + if ($cache) { + $result = Cache::get("probe_url:".$network.":".$uri); + if (!is_null($result)) { + return $result; + } + } + + if ($uid == 0) + $uid = local_user(); + + $data = self::detect($uri, $network, $uid); + + if (!isset($data["url"])) + $data["url"] = $uri; + + if ($data["photo"] != "") + $data["baseurl"] = matching_url(normalise_link($data["baseurl"]), normalise_link($data["photo"])); + else + $data["photo"] = App::get_baseurl().'/images/person-175.jpg'; + + if (!isset($data["name"]) OR ($data["name"] == "")) { + if (isset($data["nick"])) + $data["name"] = $data["nick"]; + + if ($data["name"] == "") + $data["name"] = $data["url"]; + } + + if (!isset($data["nick"]) OR ($data["nick"] == "")) { + $data["nick"] = strtolower($data["name"]); + + if (strpos($data['nick'], ' ')) + $data['nick'] = trim(substr($data['nick'], 0, strpos($data['nick'], ' '))); + } + + if (!isset($data["network"])) + $data["network"] = NETWORK_PHANTOM; + + $data = self::rearrange_data($data); + + // Only store into the cache if the value seems to be valid + if (!in_array($data['network'], array(NETWORK_PHANTOM, NETWORK_MAIL))) { + Cache::set("probe_url:".$network.":".$uri, $data, CACHE_DAY); + + /// @todo temporary fix - we need a real contact update function that updates only changing fields + /// The biggest problem is the avatar picture that could have a reduced image size. + /// It should only be updated if the existing picture isn't existing anymore. + if (($data['network'] != NETWORK_FEED) AND ($mode == PROBE_NORMAL) AND + $data["name"] AND $data["nick"] AND $data["url"] AND $data["addr"] AND $data["poll"]) + q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `url` = '%s', `addr` = '%s', + `notify` = '%s', `poll` = '%s', `alias` = '%s', `success_update` = '%s' + WHERE `nurl` = '%s' AND NOT `self` AND `uid` = 0", + dbesc($data["name"]), + dbesc($data["nick"]), + dbesc($data["url"]), + dbesc($data["addr"]), + dbesc($data["notify"]), + dbesc($data["poll"]), + dbesc($data["alias"]), + dbesc(datetime_convert()), + dbesc(normalise_link($data['url'])) + ); + } + return $data; + } + + /** + * @brief Fetch information (protocol endpoints and user information) about a given uri + * + * This function is only called by the "uri" function that adds caching and rearranging of data. + * + * @param string $uri Address that should be probed + * @param string $network Test for this specific network + * @param integer $uid User ID for the probe (only used for mails) + * + * @return array uri data + */ + private function detect($uri, $network, $uid) { + if (strstr($uri, '@')) { + // If the URI starts with "mailto:" then jump directly to the mail detection + if (strpos($url,'mailto:') !== false) { + $uri = str_replace('mailto:', '', $url); + return self::mail($uri, $uid); + } + + if ($network == NETWORK_MAIL) + return self::mail($uri, $uid); + + // Remove "acct:" from the URI + $uri = str_replace('acct:', '', $uri); + + $host = substr($uri,strpos($uri, '@') + 1); + $nick = substr($uri,0, strpos($uri, '@')); + + if (strpos($uri, '@twitter.com')) + return array("network" => NETWORK_TWITTER); + + $lrdd = self::xrd($host); + + if (!$lrdd) + return self::mail($uri, $uid); + + $addr = $uri; + } else { + $parts = parse_url($uri); + if (!isset($parts["scheme"]) OR + !isset($parts["host"]) OR + !isset($parts["path"])) + return false; + + /// @todo: Ports? + $host = $parts["host"]; + + if ($host == 'twitter.com') + return array("network" => NETWORK_TWITTER); + + $lrdd = self::xrd($host); + + $path_parts = explode("/", trim($parts["path"], "/")); + + while (!$lrdd AND (sizeof($path_parts) > 1)) { + $host .= "/".array_shift($path_parts); + $lrdd = self::xrd($host); + } + if (!$lrdd) + return self::feed($uri); + + $nick = array_pop($path_parts); + $addr = $nick."@".$host; + } + $webfinger = false; + + /// @todo Do we need the prefix "acct:" or "acct://"? + + foreach ($lrdd AS $key => $link) { + if ($webfinger) + continue; + + if (!in_array($key, array("lrdd", "lrdd-xml", "lrdd-json"))) + continue; + + // Try webfinger with the address (user@domain.tld) + $path = str_replace('{uri}', urlencode($addr), $link); + $webfinger = self::webfinger($path); + + // Mastodon needs to have it with "acct:" + if (!$webfinger) { + $path = str_replace('{uri}', urlencode("acct:".$addr), $link); + $webfinger = self::webfinger($path); + } + + // If webfinger wasn't successful then try it with the URL - possibly in the format https://... + if (!$webfinger AND ($uri != $addr)) { + $path = str_replace('{uri}', urlencode($uri), $link); + $webfinger = self::webfinger($path); + + // Since the detection with the address wasn't successful, we delete it. + if ($webfinger) { + $nick = ""; + $addr = ""; + } + } + + } + if (!$webfinger) + return self::feed($uri); + + $result = false; + + logger("Probing ".$uri, LOGGER_DEBUG); + + if (in_array($network, array("", NETWORK_DFRN))) + $result = self::dfrn($webfinger); + if ((!$result AND ($network == "")) OR ($network == NETWORK_DIASPORA)) + $result = self::diaspora($webfinger); + if ((!$result AND ($network == "")) OR ($network == NETWORK_OSTATUS)) + $result = self::ostatus($webfinger); + if ((!$result AND ($network == "")) OR ($network == NETWORK_PUMPIO)) + $result = self::pumpio($webfinger); + if ((!$result AND ($network == "")) OR ($network == NETWORK_FEED)) + $result = self::feed($uri); + else { + // We overwrite the detected nick with our try if the previois routines hadn't detected it. + // Additionally it is overwritten when the nickname doesn't make sense (contains spaces). + if ((!isset($result["nick"]) OR ($result["nick"] == "") OR (strstr($result["nick"], " "))) AND ($nick != "")) + $result["nick"] = $nick; + + if ((!isset($result["addr"]) OR ($result["addr"] == "")) AND ($addr != "")) + $result["addr"] = $addr; + } + + logger($uri." is ".$result["network"], LOGGER_DEBUG); + + if (!isset($result["baseurl"]) OR ($result["baseurl"] == "")) { + $pos = strpos($result["url"], $host); + if ($pos) + $result["baseurl"] = substr($result["url"], 0, $pos).$host; + } + + return $result; + } + + /** + * @brief Perform a webfinger request. + * + * For details see RFC 7033: + * + * @param string $url Address that should be probed + * + * @return array webfinger data + */ + private function webfinger($url) { + + $xrd_timeout = Config::get('system','xrd_timeout', 20); + $redirects = 0; + + $data = fetch_url($url, false, $redirects, $xrd_timeout, "application/xrd+xml"); + $xrd = parse_xml_string($data, false); + + if (!is_object($xrd)) { + // If it is not XML, maybe it is JSON + $webfinger = json_decode($data, true); + + if (!isset($webfinger["links"])) + return false; + + return $webfinger; + } + + $xrd_arr = xml::element_to_array($xrd); + if (!isset($xrd_arr["xrd"]["link"])) + return false; + + $webfinger = array(); + + if (isset($xrd_arr["xrd"]["subject"])) + $webfinger["subject"] = $xrd_arr["xrd"]["subject"]; + + if (isset($xrd_arr["xrd"]["alias"])) + $webfinger["aliases"] = $xrd_arr["xrd"]["alias"]; + + $webfinger["links"] = array(); + + foreach ($xrd_arr["xrd"]["link"] AS $value => $data) { + if (isset($data["@attributes"])) + $attributes = $data["@attributes"]; + elseif ($value == "@attributes") + $attributes = $data; + else + continue; + + $webfinger["links"][] = $attributes; + } + return $webfinger; + } + + /** + * @brief Poll the Friendica specific noscrape page. + * + * "noscrape" is a faster alternative to fetch the data from the hcard. + * This functionality was originally created for the directory. + * + * @param string $noscrape Link to the noscrape page + * @param array $data The already fetched data + * + * @return array noscrape data + */ + private function poll_noscrape($noscrape, $data) { + $content = fetch_url($noscrape); + if (!$content) + return false; + + $json = json_decode($content, true); + if (!is_array($json)) + return false; + + if (isset($json["fn"])) + $data["name"] = $json["fn"]; + + if (isset($json["addr"])) + $data["addr"] = $json["addr"]; + + if (isset($json["nick"])) + $data["nick"] = $json["nick"]; + + if (isset($json["comm"])) + $data["community"] = $json["comm"]; + + if (isset($json["tags"])) { + $keywords = implode(" ", $json["tags"]); + if ($keywords != "") + $data["keywords"] = $keywords; + } + + $location = formatted_location($json); + if ($location) + $data["location"] = $location; + + if (isset($json["about"])) + $data["about"] = $json["about"]; + + if (isset($json["key"])) + $data["pubkey"] = $json["key"]; + + if (isset($json["photo"])) + $data["photo"] = $json["photo"]; + + if (isset($json["dfrn-request"])) + $data["request"] = $json["dfrn-request"]; + + if (isset($json["dfrn-confirm"])) + $data["confirm"] = $json["dfrn-confirm"]; + + if (isset($json["dfrn-notify"])) + $data["notify"] = $json["dfrn-notify"]; + + if (isset($json["dfrn-poll"])) + $data["poll"] = $json["dfrn-poll"]; + + return $data; + } + + /** + * @brief Check for valid DFRN data + * + * @param array $data DFRN data + * + * @return int Number of errors + */ + public static function valid_dfrn($data) { + $errors = 0; + if(!isset($data['key'])) + $errors ++; + if(!isset($data['dfrn-request'])) + $errors ++; + if(!isset($data['dfrn-confirm'])) + $errors ++; + if(!isset($data['dfrn-notify'])) + $errors ++; + if(!isset($data['dfrn-poll'])) + $errors ++; + return $errors; + } + + /** + * @brief Fetch data from a DFRN profile page and via "noscrape" + * + * @param string $profile Link to the profile page + * + * @return array profile data + */ + public static function profile($profile) { + + $data = array(); + + logger("Check profile ".$profile, LOGGER_DEBUG); + + // Fetch data via noscrape - this is faster + $noscrape = str_replace(array("/hcard/", "/profile/"), "/noscrape/", $profile); + $data = self::poll_noscrape($noscrape, $data); + + if (!isset($data["notify"]) OR !isset($data["confirm"]) OR + !isset($data["request"]) OR !isset($data["poll"]) OR + !isset($data["poco"]) OR !isset($data["name"]) OR + !isset($data["photo"])) + $data = self::poll_hcard($profile, $data, true); + + $prof_data = array(); + $prof_data["addr"] = $data["addr"]; + $prof_data["nick"] = $data["nick"]; + $prof_data["dfrn-request"] = $data["request"]; + $prof_data["dfrn-confirm"] = $data["confirm"]; + $prof_data["dfrn-notify"] = $data["notify"]; + $prof_data["dfrn-poll"] = $data["poll"]; + $prof_data["dfrn-poco"] = $data["poco"]; + $prof_data["photo"] = $data["photo"]; + $prof_data["fn"] = $data["name"]; + $prof_data["key"] = $data["pubkey"]; + + logger("Result for profile ".$profile.": ".print_r($prof_data, true), LOGGER_DEBUG); + + return $prof_data; + } + + /** + * @brief Check for DFRN contact + * + * @param array $webfinger Webfinger data + * + * @return array DFRN data + */ + private function dfrn($webfinger) { + + $hcard = ""; + $data = array(); + foreach ($webfinger["links"] AS $link) { + if (($link["rel"] == NAMESPACE_DFRN) AND ($link["href"] != "")) + $data["network"] = NETWORK_DFRN; + elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != "")) + $data["poll"] = $link["href"]; + elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND + ($link["type"] == "text/html") AND ($link["href"] != "")) + $data["url"] = $link["href"]; + elseif (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != "")) + $hcard = $link["href"]; + elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != "")) + $data["poco"] = $link["href"]; + elseif (($link["rel"] == "http://webfinger.net/rel/avatar") AND ($link["href"] != "")) + $data["photo"] = $link["href"]; + + elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != "")) + $data["baseurl"] = trim($link["href"], '/'); + elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != "")) + $data["guid"] = $link["href"]; + elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) { + $data["pubkey"] = base64_decode($link["href"]); + + //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA")) + if (strstr($data["pubkey"], 'RSA ')) + $data["pubkey"] = rsatopem($data["pubkey"]); + } + } + + if (!isset($data["network"]) OR ($hcard == "")) + return false; + + // Fetch data via noscrape - this is faster + $noscrape = str_replace("/hcard/", "/noscrape/", $hcard); + $data = self::poll_noscrape($noscrape, $data); + + if (isset($data["notify"]) AND isset($data["confirm"]) AND isset($data["request"]) AND + isset($data["poll"]) AND isset($data["name"]) AND isset($data["photo"])) + return $data; + + $data = self::poll_hcard($hcard, $data, true); + + return $data; + } + + /** + * @brief Poll the hcard page (Diaspora and Friendica specific) + * + * @param string $hcard Link to the hcard page + * @param array $data The already fetched data + * @param boolean $dfrn Poll DFRN specific data + * + * @return array hcard data + */ + private function poll_hcard($hcard, $data, $dfrn = false) { + + $content = fetch_url($hcard); + if (!$content) + return false; + + $doc = new DOMDocument(); + if (!@$doc->loadHTML($content)) + return false; + + $xpath = new DomXPath($doc); + + $vcards = $xpath->query("//div[contains(concat(' ', @class, ' '), ' vcard ')]"); + if (!is_object($vcards)) + return false; + + if ($vcards->length > 0) { + $vcard = $vcards->item(0); + + // We have to discard the guid from the hcard in favour of the guid from lrdd + // Reason: Hubzilla doesn't use the value "uid" in the hcard like Diaspora does. + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' uid ')]", $vcard); // */ + if (($search->length > 0) AND ($data["guid"] == "")) + $data["guid"] = $search->item(0)->nodeValue; + + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' nickname ')]", $vcard); // */ + if ($search->length > 0) + $data["nick"] = $search->item(0)->nodeValue; + + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' fn ')]", $vcard); // */ + if ($search->length > 0) + $data["name"] = $search->item(0)->nodeValue; + + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' searchable ')]", $vcard); // */ + if ($search->length > 0) + $data["searchable"] = $search->item(0)->nodeValue; + + $search = $xpath->query("//*[contains(concat(' ', @class, ' '), ' key ')]", $vcard); // */ + if ($search->length > 0) { + $data["pubkey"] = $search->item(0)->nodeValue; + if (strstr($data["pubkey"], 'RSA ')) + $data["pubkey"] = rsatopem($data["pubkey"]); + } + + $search = $xpath->query("//*[@id='pod_location']", $vcard); // */ + if ($search->length > 0) + $data["baseurl"] = trim($search->item(0)->nodeValue, "/"); + } + + $avatar = array(); + $photos = $xpath->query("//*[contains(concat(' ', @class, ' '), ' photo ') or contains(concat(' ', @class, ' '), ' avatar ')]", $vcard); // */ + foreach ($photos AS $photo) { + $attr = array(); + foreach ($photo->attributes as $attribute) { + $attr[$attribute->name] = trim($attribute->value); + } + + if (isset($attr["src"]) AND isset($attr["width"])) { + $avatar[$attr["width"]] = $attr["src"]; + } + + // We don't have a width. So we just take everything that we got. + // This is a Hubzilla workaround which doesn't send a width. + if ((sizeof($avatar) == 0) AND isset($attr["src"])) { + $avatar[] = $attr["src"]; + } + } + + if (sizeof($avatar)) { + ksort($avatar); + $data["photo"] = array_pop($avatar); + } + + if ($dfrn) { + // Poll DFRN specific data + $search = $xpath->query("//link[contains(concat(' ', @rel), ' dfrn-')]"); + if ($search->length > 0) { + foreach ($search AS $link) { + //$data["request"] = $search->item(0)->nodeValue; + $attr = array(); + foreach ($link->attributes as $attribute) + $attr[$attribute->name] = trim($attribute->value); + + $data[substr($attr["rel"], 5)] = $attr["href"]; + } + } + + // Older Friendica versions had used the "uid" field differently than newer versions + if ($data["nick"] == $data["guid"]) + unset($data["guid"]); + } + + + return $data; + } + + /** + * @brief Check for Diaspora contact + * + * @param array $webfinger Webfinger data + * + * @return array Diaspora data + */ + private function diaspora($webfinger) { + + $hcard = ""; + $data = array(); + foreach ($webfinger["links"] AS $link) { + if (($link["rel"] == "http://microformats.org/profile/hcard") AND ($link["href"] != "")) + $hcard = $link["href"]; + elseif (($link["rel"] == "http://joindiaspora.com/seed_location") AND ($link["href"] != "")) + $data["baseurl"] = trim($link["href"], '/'); + elseif (($link["rel"] == "http://joindiaspora.com/guid") AND ($link["href"] != "")) + $data["guid"] = $link["href"]; + elseif (($link["rel"] == "http://webfinger.net/rel/profile-page") AND + ($link["type"] == "text/html") AND ($link["href"] != "")) + $data["url"] = $link["href"]; + elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != "")) + $data["poll"] = $link["href"]; + elseif (($link["rel"] == NAMESPACE_POCO) AND ($link["href"] != "")) + $data["poco"] = $link["href"]; + elseif (($link["rel"] == "salmon") AND ($link["href"] != "")) + $data["notify"] = $link["href"]; + elseif (($link["rel"] == "diaspora-public-key") AND ($link["href"] != "")) { + $data["pubkey"] = base64_decode($link["href"]); + + //if (strstr($data["pubkey"], 'RSA ') OR ($link["type"] == "RSA")) + if (strstr($data["pubkey"], 'RSA ')) + $data["pubkey"] = rsatopem($data["pubkey"]); + } + } + + if (!isset($data["url"]) OR ($hcard == "")) + return false; + + if (is_array($webfinger["aliases"])) + foreach ($webfinger["aliases"] AS $alias) + if (normalise_link($alias) != normalise_link($data["url"]) AND !strstr($alias, "@")) + $data["alias"] = $alias; + + // Fetch further information from the hcard + $data = self::poll_hcard($hcard, $data); + + if (!$data) + return false; + + if (isset($data["url"]) AND isset($data["guid"]) AND isset($data["baseurl"]) AND + isset($data["pubkey"]) AND ($hcard != "")) { + $data["network"] = NETWORK_DIASPORA; + + // The Diaspora handle must always be lowercase + $data["addr"] = strtolower($data["addr"]); + + // We have to overwrite the detected value for "notify" since Hubzilla doesn't send it + $data["notify"] = $data["baseurl"]."/receive/users/".$data["guid"]; + $data["batch"] = $data["baseurl"]."/receive/public"; + } else + return false; + + return $data; + } + + /** + * @brief Check for OStatus contact + * + * @param array $webfinger Webfinger data + * + * @return array OStatus data + */ + private function ostatus($webfinger) { + + $data = array(); + if (is_array($webfinger["aliases"])) + foreach($webfinger["aliases"] AS $alias) + if (strstr($alias, "@")) + $data["addr"] = str_replace('acct:', '', $alias); + + if (is_string($webfinger["subject"]) AND strstr($webfinger["subject"], "@")) + $data["addr"] = str_replace('acct:', '', $webfinger["subject"]); + + $pubkey = ""; + foreach ($webfinger["links"] AS $link) { + if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND + ($link["type"] == "text/html") AND ($link["href"] != "")) + $data["url"] = $link["href"]; + elseif (($link["rel"] == "salmon") AND ($link["href"] != "")) + $data["notify"] = $link["href"]; + elseif (($link["rel"] == NAMESPACE_FEED) AND ($link["href"] != "")) + $data["poll"] = $link["href"]; + elseif (($link["rel"] == "magic-public-key") AND ($link["href"] != "")) { + $pubkey = $link["href"]; + + if (substr($pubkey, 0, 5) === 'data:') { + if (strstr($pubkey, ',')) + $pubkey = substr($pubkey, strpos($pubkey, ',') + 1); + else + $pubkey = substr($pubkey, 5); + } elseif (normalise_link($pubkey) == 'http://') + $pubkey = fetch_url($pubkey); + + $key = explode(".", $pubkey); + + if (sizeof($key) >= 3) { + $m = base64url_decode($key[1]); + $e = base64url_decode($key[2]); + $data["pubkey"] = metopem($m,$e); + } + + } + } + + if (isset($data["notify"]) AND isset($data["pubkey"]) AND + isset($data["poll"]) AND isset($data["url"])) { + $data["network"] = NETWORK_OSTATUS; + } else + return false; + + // Fetch all additional data from the feed + $feed = fetch_url($data["poll"]); + $feed_data = feed_import($feed,$dummy1,$dummy2, $dummy3, true); + if (!$feed_data) + return false; + + if ($feed_data["header"]["author-name"] != "") + $data["name"] = $feed_data["header"]["author-name"]; + + if ($feed_data["header"]["author-nick"] != "") + $data["nick"] = $feed_data["header"]["author-nick"]; + + if ($feed_data["header"]["author-avatar"] != "") + $data["photo"] = $feed_data["header"]["author-avatar"]; + + if ($feed_data["header"]["author-id"] != "") + $data["alias"] = $feed_data["header"]["author-id"]; + + if ($feed_data["header"]["author-location"] != "") + $data["location"] = $feed_data["header"]["author-location"]; + + if ($feed_data["header"]["author-about"] != "") + $data["about"] = $feed_data["header"]["author-about"]; + + // OStatus has serious issues when the the url doesn't fit (ssl vs. non ssl) + // So we take the value that we just fetched, although the other one worked as well + if ($feed_data["header"]["author-link"] != "") + $data["url"] = $feed_data["header"]["author-link"]; + + /// @todo Fetch location and "about" from the feed as well + return $data; + } + + /** + * @brief Fetch data from a pump.io profile page + * + * @param string $profile Link to the profile page + * + * @return array profile data + */ + private function pumpio_profile_data($profile) { + + $doc = new DOMDocument(); + if (!@$doc->loadHTMLFile($profile)) + return false; + + $xpath = new DomXPath($doc); + + $data = array(); + + // This is ugly - but pump.io doesn't seem to know a better way for it + $data["name"] = trim($xpath->query("//h1[@class='media-header']")->item(0)->nodeValue); + $pos = strpos($data["name"], chr(10)); + if ($pos) + $data["name"] = trim(substr($data["name"], 0, $pos)); + + $avatar = $xpath->query("//img[@class='img-rounded media-object']")->item(0); + if ($avatar) + foreach ($avatar->attributes as $attribute) + if ($attribute->name == "src") + $data["photo"] = trim($attribute->value); + + $data["location"] = $xpath->query("//p[@class='location']")->item(0)->nodeValue; + $data["about"] = $xpath->query("//p[@class='summary']")->item(0)->nodeValue; + + return $data; + } + + /** + * @brief Check for pump.io contact + * + * @param array $webfinger Webfinger data + * + * @return array pump.io data + */ + private function pumpio($webfinger) { + + $data = array(); + foreach ($webfinger["links"] AS $link) { + if (($link["rel"] == "http://webfinger.net/rel/profile-page") AND + ($link["type"] == "text/html") AND ($link["href"] != "")) + $data["url"] = $link["href"]; + elseif (($link["rel"] == "activity-inbox") AND ($link["href"] != "")) + $data["notify"] = $link["href"]; + elseif (($link["rel"] == "activity-outbox") AND ($link["href"] != "")) + $data["poll"] = $link["href"]; + elseif (($link["rel"] == "dialback") AND ($link["href"] != "")) + $data["dialback"] = $link["href"]; + } + if (isset($data["poll"]) AND isset($data["notify"]) AND + isset($data["dialback"]) AND isset($data["url"])) { + + // by now we use these fields only for the network type detection + // So we unset all data that isn't used at the moment + unset($data["dialback"]); + + $data["network"] = NETWORK_PUMPIO; + } else + return false; + + $profile_data = self::pumpio_profile_data($data["url"]); + + if (!$profile_data) + return false; + + $data = array_merge($data, $profile_data); + + return $data; + } + + /** + * @brief Check page for feed link + * + * @param string $url Page link + * + * @return string feed link + */ + private function get_feed_link($url) { + $doc = new DOMDocument(); + + if (!@$doc->loadHTMLFile($url)) + return false; + + $xpath = new DomXPath($doc); + + //$feeds = $xpath->query("/html/head/link[@type='application/rss+xml']"); + $feeds = $xpath->query("/html/head/link[@type='application/rss+xml' and @rel='alternate']"); + if (!is_object($feeds)) + return false; + + if ($feeds->length == 0) + return false; + + $feed_url = ""; + + foreach ($feeds AS $feed) { + $attr = array(); + foreach ($feed->attributes as $attribute) + $attr[$attribute->name] = trim($attribute->value); + + if ($feed_url == "") + $feed_url = $attr["href"]; + } + + return $feed_url; + } + + /** + * @brief Check for feed contact + * + * @param string $url Profile link + * @param boolean $probe Do a probe if the page contains a feed link + * + * @return array feed data + */ + private function feed($url, $probe = true) { + $feed = fetch_url($url); + $feed_data = feed_import($feed, $dummy1, $dummy2, $dummy3, true); + + if (!$feed_data) { + if (!$probe) + return false; + + $feed_url = self::get_feed_link($url); + + if (!$feed_url) + return false; + + return self::feed($feed_url, false); + } + + if ($feed_data["header"]["author-name"] != "") + $data["name"] = $feed_data["header"]["author-name"]; + + if ($feed_data["header"]["author-nick"] != "") + $data["nick"] = $feed_data["header"]["author-nick"]; + + if ($feed_data["header"]["author-avatar"] != "") + $data["photo"] = $feed_data["header"]["author-avatar"]; + + if ($feed_data["header"]["author-id"] != "") + $data["alias"] = $feed_data["header"]["author-id"]; + + $data["url"] = $url; + $data["poll"] = $url; + + if ($feed_data["header"]["author-link"] != "") + $data["baseurl"] = $feed_data["header"]["author-link"]; + else + $data["baseurl"] = $data["url"]; + + $data["network"] = NETWORK_FEED; + + return $data; + } + + /** + * @brief Check for mail contact + * + * @param string $uri Profile link + * @param integer $uid User ID + * + * @return array mail data + */ + private function mail($uri, $uid) { + + if (!validate_email($uri)) + return false; + + $x = q("SELECT `prvkey` FROM `user` WHERE `uid` = %d LIMIT 1", intval($uid)); + + $r = q("SELECT * FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1", intval($uid)); + + if (dbm::is_result($x) && dbm::is_result($r)) { + $mailbox = construct_mailbox_name($r[0]); + $password = ''; + openssl_private_decrypt(hex2bin($r[0]['pass']), $password,$x[0]['prvkey']); + $mbox = email_connect($mailbox,$r[0]['user'], $password); + if(!mbox) + return false; + } + + $msgs = email_poll($mbox, $uri); + logger('searching '.$uri.', '.count($msgs).' messages found.', LOGGER_DEBUG); + + if (!count($msgs)) + return false; + + $data = array(); + + $data["addr"] = $uri; + $data["network"] = NETWORK_MAIL; + $data["name"] = substr($uri, 0, strpos($uri,'@')); + $data["nick"] = $data["name"]; + $data["photo"] = avatar_img($uri); + + $phost = substr($uri, strpos($uri,'@') + 1); + $data["url"] = 'http://'.$phost."/".$data["nick"]; + $data["notify"] = 'smtp '.random_string(); + $data["poll"] = 'email '.random_string(); + + $x = email_msg_meta($mbox, $msgs[0]); + if(stristr($x[0]->from, $uri)) + $adr = imap_rfc822_parse_adrlist($x[0]->from, ''); + elseif(stristr($x[0]->to, $uri)) + $adr = imap_rfc822_parse_adrlist($x[0]->to, ''); + if(isset($adr)) { + foreach($adr as $feadr) { + if((strcasecmp($feadr->mailbox, $data["name"]) == 0) + &&(strcasecmp($feadr->host, $phost) == 0) + && (strlen($feadr->personal))) { + + $personal = imap_mime_header_decode($feadr->personal); + $data["name"] = ""; + foreach($personal as $perspart) + if ($perspart->charset != "default") + $data["name"] .= iconv($perspart->charset, 'UTF-8//IGNORE', $perspart->text); + else + $data["name"] .= $perspart->text; + + $data["name"] = notags($data["name"]); + } + } + } + imap_close($mbox); + + return $data; + } +} +?> diff --git a/include/Scrape.php b/include/Scrape.php index 6ee3dabfca..bb9af60d70 100644 --- a/include/Scrape.php +++ b/include/Scrape.php @@ -1,317 +1,5 @@ get_curl_headers(); - logger('scrape_dfrn: headers=' . $headers, LOGGER_DEBUG); - - - $lines = explode("\n",$headers); - if(count($lines)) { - foreach($lines as $line) { - // don't try and run feeds through the html5 parser - if(stristr($line,'content-type:') && ((stristr($line,'application/atom+xml')) || (stristr($line,'application/rss+xml')))) - return ret; - } - } - - try { - $dom = HTML5_Parser::parse($s); - } catch (DOMException $e) { - logger('scrape_dfrn: parse error: ' . $e); - } - - if(! $dom) - return $ret; - - $items = $dom->getElementsByTagName('link'); - - // get DFRN link elements - - foreach($items as $item) { - $x = $item->getAttribute('rel'); - if(($x === 'alternate') && ($item->getAttribute('type') === 'application/atom+xml')) - $ret['feed_atom'] = $item->getAttribute('href'); - if(substr($x,0,5) == "dfrn-") { - $ret[$x] = $item->getAttribute('href'); - } - if($x === 'lrdd') { - $decoded = urldecode($item->getAttribute('href')); - if(preg_match('/acct:([^@]*)@/',$decoded,$matches)) - $ret['nick'] = $matches[1]; - } - } - - // Pull out hCard profile elements - - $largest_photo = 0; - - $items = $dom->getElementsByTagName('*'); - foreach($items as $item) { - if(attribute_contains($item->getAttribute('class'), 'vcard')) { - $level2 = $item->getElementsByTagName('*'); - foreach($level2 as $x) { - if(attribute_contains($x->getAttribute('class'),'fn')) { - $ret['fn'] = $x->textContent; - } - if((attribute_contains($x->getAttribute('class'),'photo')) - || (attribute_contains($x->getAttribute('class'),'avatar'))) { - $size = intval($x->getAttribute('width')); - // dfrn prefers 175, so if we find this, we set largest_size so it can't be topped. - if(($size > $largest_photo) || ($size == 175) || (! $largest_photo)) { - $ret['photo'] = $x->getAttribute('src'); - $largest_photo = (($size == 175) ? 9999 : $size); - } - } - if(attribute_contains($x->getAttribute('class'),'key')) { - $ret['key'] = $x->textContent; - } - } - } - } - - return $ret; -}} - - - - - - -if(! function_exists('validate_dfrn')) { -function validate_dfrn($a) { - $errors = 0; - if(! x($a,'key')) - $errors ++; - if(! x($a,'dfrn-request')) - $errors ++; - if(! x($a,'dfrn-confirm')) - $errors ++; - if(! x($a,'dfrn-notify')) - $errors ++; - if(! x($a,'dfrn-poll')) - $errors ++; - return $errors; -}} - -if(! function_exists('scrape_meta')) { -function scrape_meta($url) { - - $a = get_app(); - - $ret = array(); - - logger('scrape_meta: url=' . $url); - - $s = fetch_url($url); - - if(! $s) - return $ret; - - $headers = $a->get_curl_headers(); - logger('scrape_meta: headers=' . $headers, LOGGER_DEBUG); - - $lines = explode("\n",$headers); - if(count($lines)) { - foreach($lines as $line) { - // don't try and run feeds through the html5 parser - if(stristr($line,'content-type:') && ((stristr($line,'application/atom+xml')) || (stristr($line,'application/rss+xml')))) - return ret; - } - } - - try { - $dom = HTML5_Parser::parse($s); - } catch (DOMException $e) { - logger('scrape_meta: parse error: ' . $e); - } - - if(! $dom) - return $ret; - - $items = $dom->getElementsByTagName('meta'); - - // get DFRN link elements - - foreach($items as $item) { - $x = $item->getAttribute('name'); - if(substr($x,0,5) == "dfrn-") - $ret[$x] = $item->getAttribute('content'); - } - - return $ret; -}} - - -if(! function_exists('scrape_vcard')) { -function scrape_vcard($url) { - - $a = get_app(); - - $ret = array(); - - logger('scrape_vcard: url=' . $url); - - $s = fetch_url($url); - - if(! $s) - return $ret; - - $headers = $a->get_curl_headers(); - $lines = explode("\n",$headers); - if(count($lines)) { - foreach($lines as $line) { - // don't try and run feeds through the html5 parser - if(stristr($line,'content-type:') && ((stristr($line,'application/atom+xml')) || (stristr($line,'application/rss+xml')))) - return ret; - } - } - - try { - $dom = HTML5_Parser::parse($s); - } catch (DOMException $e) { - logger('scrape_vcard: parse error: ' . $e); - } - - if(! $dom) - return $ret; - - // Pull out hCard profile elements - - $largest_photo = 0; - - $items = $dom->getElementsByTagName('*'); - foreach($items as $item) { - if(attribute_contains($item->getAttribute('class'), 'vcard')) { - $level2 = $item->getElementsByTagName('*'); - foreach($level2 as $x) { - if(attribute_contains($x->getAttribute('class'),'fn')) - $ret['fn'] = $x->textContent; - if((attribute_contains($x->getAttribute('class'),'photo')) - || (attribute_contains($x->getAttribute('class'),'avatar'))) { - $size = intval($x->getAttribute('width')); - if(($size > $largest_photo) || (! $largest_photo)) { - $ret['photo'] = $x->getAttribute('src'); - $largest_photo = $size; - } - } - if((attribute_contains($x->getAttribute('class'),'nickname')) - || (attribute_contains($x->getAttribute('class'),'uid'))) { - $ret['nick'] = $x->textContent; - } - } - } - } - - return $ret; -}} - - -if(! function_exists('scrape_feed')) { -function scrape_feed($url) { - - $a = get_app(); - - $ret = array(); - $s = fetch_url($url); - - $headers = $a->get_curl_headers(); - $code = $a->get_curl_code(); - - logger('scrape_feed: returns: ' . $code . ' headers=' . $headers, LOGGER_DEBUG); - - if(! $s) { - logger('scrape_feed: no data returned for ' . $url); - return $ret; - } - - - $lines = explode("\n",$headers); - if(count($lines)) { - foreach($lines as $line) { - if(stristr($line,'content-type:')) { - if(stristr($line,'application/atom+xml') || stristr($s,'')) { - $ret['feed_rss'] = $url; - return $ret; - } - } - - $basename = implode('/', array_slice(explode('/',$url),0,3)) . '/'; - - $doc = new DOMDocument(); - @$doc->loadHTML($s); - $xpath = new DomXPath($doc); - - $base = $xpath->query("//base"); - foreach ($base as $node) { - $attr = array(); - - if ($node->attributes->length) - foreach ($node->attributes as $attribute) - $attr[$attribute->name] = $attribute->value; - - if ($attr["href"] != "") - $basename = $attr["href"] ; - } - - $list = $xpath->query("//link"); - foreach ($list as $node) { - $attr = array(); - - if ($node->attributes->length) - foreach ($node->attributes as $attribute) - $attr[$attribute->name] = $attribute->value; - - if (($attr["rel"] == "alternate") AND ($attr["type"] == "application/atom+xml")) - $ret["feed_atom"] = $attr["href"]; - - if (($attr["rel"] == "alternate") AND ($attr["type"] == "application/rss+xml")) - $ret["feed_rss"] = $attr["href"]; - } - - // Drupal and perhaps others only provide relative URLs. Turn them into absolute. - - if(x($ret,'feed_atom') && (! strstr($ret['feed_atom'],'://'))) - $ret['feed_atom'] = $basename . $ret['feed_atom']; - if(x($ret,'feed_rss') && (! strstr($ret['feed_rss'],'://'))) - $ret['feed_rss'] = $basename . $ret['feed_rss']; - - return $ret; -}} - +require_once('include/Probe.php'); /** * @@ -331,564 +19,17 @@ function scrape_feed($url) { * */ - -define ( 'PROBE_NORMAL', 0); -define ( 'PROBE_DIASPORA', 1); +define('PROBE_NORMAL', 0); +define('PROBE_DIASPORA', 1); function probe_url($url, $mode = PROBE_NORMAL, $level = 1) { - require_once('include/email.php'); - $result = array(); + if ($mode == PROBE_DIASPORA) + $network = NETWORK_DIASPORA; + else + $network = ""; - if(! $url) - return $result; + $data = Probe::uri($url, $network); - $result = Cache::get("probe_url:".$mode.":".$url); - if (!is_null($result)) { - $result = unserialize($result); - return $result; - } - - $network = null; - $diaspora = false; - $diaspora_base = ''; - $diaspora_guid = ''; - $diaspora_key = ''; - $has_lrdd = false; - $email_conversant = false; - $connectornetworks = false; - $appnet = false; - - if (strpos($url,'twitter.com')) { - $connectornetworks = true; - $network = NETWORK_TWITTER; - } - - // Twitter is deactivated since twitter closed its old API - //$twitter = ((strpos($url,'twitter.com') !== false) ? true : false); - $lastfm = ((strpos($url,'last.fm/user') !== false) ? true : false); - - $at_addr = ((strpos($url,'@') !== false) ? true : false); - - if((!$appnet) && (!$lastfm) && !$connectornetworks) { - - if(strpos($url,'mailto:') !== false && $at_addr) { - $url = str_replace('mailto:','',$url); - $links = array(); - } - else - $links = lrdd($url); - - if(count($links)) { - $has_lrdd = true; - - logger('probe_url: found lrdd links: ' . print_r($links,true), LOGGER_DATA); - foreach($links as $link) { - if($link['@attributes']['rel'] === NAMESPACE_ZOT) - $zot = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === NAMESPACE_DFRN) - $dfrn = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === 'salmon') - $notify = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === NAMESPACE_FEED) - $poll = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === 'http://microformats.org/profile/hcard') - $hcard = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === 'http://webfinger.net/rel/profile-page') - $profile = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === 'http://portablecontacts.net/spec/1.0') - $poco = unamp($link['@attributes']['href']); - if($link['@attributes']['rel'] === 'http://joindiaspora.com/seed_location') { - $diaspora_base = unamp($link['@attributes']['href']); - $diaspora = true; - } - if($link['@attributes']['rel'] === 'http://joindiaspora.com/guid') { - $diaspora_guid = unamp($link['@attributes']['href']); - $diaspora = true; - } - if($link['@attributes']['rel'] === 'diaspora-public-key') { - $diaspora_key = base64_decode(unamp($link['@attributes']['href'])); - if(strstr($diaspora_key,'RSA ')) - $pubkey = rsatopem($diaspora_key); - else - $pubkey = $diaspora_key; - $diaspora = true; - } - if(($link['@attributes']['rel'] === 'http://ostatus.org/schema/1.0/subscribe') AND ($mode == PROBE_NORMAL)) { - $diaspora = false; - } - } - - // Status.Net can have more than one profile URL. We need to match the profile URL - // to a contact on incoming messages to prevent spam, and we won't know which one - // to match. So in case of two, one of them is stored as an alias. Only store URL's - // and not webfinger user@host aliases. If they've got more than two non-email style - // aliases, let's hope we're lucky and get one that matches the feed author-uri because - // otherwise we're screwed. - - foreach($links as $link) { - if($link['@attributes']['rel'] === 'alias') { - if(strpos($link['@attributes']['href'],'@') === false) { - if(isset($profile)) { - if($link['@attributes']['href'] !== $profile) - $alias = unamp($link['@attributes']['href']); - } - else - $profile = unamp($link['@attributes']['href']); - } - } - } - - // If the profile is different from the url then the url is abviously an alias - if (($alias == "") AND ($profile != "") AND !$at_addr AND (normalise_link($profile) != normalise_link($url))) - $alias = $url; - } - elseif($mode == PROBE_NORMAL) { - - // Check email - - $orig_url = $url; - if((strpos($orig_url,'@')) && validate_email($orig_url)) { - $x = q("SELECT `prvkey` FROM `user` WHERE `uid` = %d LIMIT 1", - intval(local_user()) - ); - $r = q("SELECT * FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1", - intval(local_user()) - ); - if(count($x) && count($r)) { - $mailbox = construct_mailbox_name($r[0]); - $password = ''; - openssl_private_decrypt(hex2bin($r[0]['pass']),$password,$x[0]['prvkey']); - $mbox = email_connect($mailbox,$r[0]['user'],$password); - if(! $mbox) - logger('probe_url: email_connect failed.'); - unset($password); - } - if($mbox) { - $msgs = email_poll($mbox,$orig_url); - logger('probe_url: searching ' . $orig_url . ', ' . count($msgs) . ' messages found.', LOGGER_DEBUG); - if(count($msgs)) { - $addr = $orig_url; - $network = NETWORK_MAIL; - $name = substr($url,0,strpos($url,'@')); - $phost = substr($url,strpos($url,'@')+1); - $profile = 'http://' . $phost; - // fix nick character range - $vcard = array('fn' => $name, 'nick' => $name, 'photo' => avatar_img($url)); - $notify = 'smtp ' . random_string(); - $poll = 'email ' . random_string(); - $priority = 0; - $x = email_msg_meta($mbox,$msgs[0]); - if(stristr($x[0]->from,$orig_url)) - $adr = imap_rfc822_parse_adrlist($x[0]->from,''); - elseif(stristr($x[0]->to,$orig_url)) - $adr = imap_rfc822_parse_adrlist($x[0]->to,''); - if(isset($adr)) { - foreach($adr as $feadr) { - if((strcasecmp($feadr->mailbox,$name) == 0) - &&(strcasecmp($feadr->host,$phost) == 0) - && (strlen($feadr->personal))) { - - $personal = imap_mime_header_decode($feadr->personal); - $vcard['fn'] = ""; - foreach($personal as $perspart) - if ($perspart->charset != "default") - $vcard['fn'] .= iconv($perspart->charset, 'UTF-8//IGNORE', $perspart->text); - else - $vcard['fn'] .= $perspart->text; - - $vcard['fn'] = notags($vcard['fn']); - } - } - } - } - imap_close($mbox); - } - } - } - } - - if($mode == PROBE_NORMAL) { - - if(strlen($zot)) { - $s = fetch_url($zot); - if($s) { - $j = json_decode($s); - if($j) { - $network = NETWORK_ZOT; - $vcard = array( - 'fn' => $j->fullname, - 'nick' => $j->nickname, - 'photo' => $j->photo - ); - $profile = $j->url; - $notify = $j->post; - $pubkey = $j->pubkey; - $poll = 'N/A'; - } - } - } - - - if(strlen($dfrn)) { - $ret = scrape_dfrn(($hcard) ? $hcard : $dfrn, true); - if(is_array($ret) && x($ret,'dfrn-request')) { - $network = NETWORK_DFRN; - $request = $ret['dfrn-request']; - $confirm = $ret['dfrn-confirm']; - $notify = $ret['dfrn-notify']; - $poll = $ret['dfrn-poll']; - - $vcard = array(); - $vcard['fn'] = $ret['fn']; - $vcard['nick'] = $ret['nick']; - $vcard['photo'] = $ret['photo']; - } - } - } - - if($diaspora && $diaspora_base && $diaspora_guid) { - if($mode == PROBE_DIASPORA || ! $notify) { - $notify = $diaspora_base . 'receive/users/' . $diaspora_guid; - $batch = $diaspora_base . 'receive/public' ; - } - if(strpos($url,'@')) - $addr = str_replace('acct:', '', $url); - } - - if($network !== NETWORK_ZOT && $network !== NETWORK_DFRN && $network !== NETWORK_MAIL) { - if($diaspora) - $network = NETWORK_DIASPORA; - elseif($has_lrdd AND ($notify)) - $network = NETWORK_OSTATUS; - - if(strpos($url,'@')) - $addr = str_replace('acct:', '', $url); - - $priority = 0; - - if($hcard && ! $vcard) { - $vcard = scrape_vcard($hcard); - - // Google doesn't use absolute url in profile photos - - if((x($vcard,'photo')) && substr($vcard['photo'],0,1) == '/') { - $h = @parse_url($hcard); - if($h) - $vcard['photo'] = $h['scheme'] . '://' . $h['host'] . $vcard['photo']; - } - - logger('probe_url: scrape_vcard: ' . print_r($vcard,true), LOGGER_DATA); - } - - if($diaspora && $addr) { - // Diaspora returns the name as the nick. As the nick will never be updated, - // let's use the Diaspora nickname (the first part of the handle) as the nick instead - $addr_parts = explode('@', $addr); - $vcard['nick'] = $addr_parts[0]; - } - - /* if($twitter) { - logger('twitter: setup'); - $tid = basename($url); - $tapi = 'https://api.twitter.com/1/statuses/user_timeline.rss'; - if(intval($tid)) - $poll = $tapi . '?user_id=' . $tid; - else - $poll = $tapi . '?screen_name=' . $tid; - $profile = 'http://twitter.com/#!/' . $tid; - //$vcard['photo'] = 'https://api.twitter.com/1/users/profile_image/' . $tid; - $vcard['photo'] = 'https://api.twitter.com/1/users/profile_image?screen_name=' . $tid . '&size=bigger'; - $vcard['nick'] = $tid; - $vcard['fn'] = $tid; - } */ - - if($lastfm) { - $profile = $url; - $poll = str_replace(array('www.','last.fm/'),array('','ws.audioscrobbler.com/1.0/'),$url) . '/recenttracks.rss'; - $vcard['nick'] = basename($url); - $vcard['fn'] = $vcard['nick'] . t(' on Last.fm'); - $network = NETWORK_FEED; - } - - if(! x($vcard,'fn')) - if(x($vcard,'nick')) - $vcard['fn'] = $vcard['nick']; - - $check_feed = false; - - if(stristr($url,'tumblr.com') && (! stristr($url,'/rss'))) { - $poll = $url . '/rss'; - $check_feed = true; - // Will leave it to others to figure out how to grab the avatar, which is on the $url page in the open graph meta links - } - - if($appnet || ! $poll) - $check_feed = true; - if((! isset($vcard)) || (! x($vcard,'fn')) || (! $profile)) - $check_feed = true; - if(($at_addr) && (! count($links))) - $check_feed = false; - - if ($connectornetworks) - $check_feed = false; - - if($check_feed) { - - $feedret = scrape_feed(($poll) ? $poll : $url); - - logger('probe_url: scrape_feed ' . (($poll)? $poll : $url) . ' returns: ' . print_r($feedret,true), LOGGER_DATA); - if(count($feedret) && ($feedret['feed_atom'] || $feedret['feed_rss'])) { - $poll = ((x($feedret,'feed_atom')) ? unamp($feedret['feed_atom']) : unamp($feedret['feed_rss'])); - if(! x($vcard)) - $vcard = array(); - } - - if(x($feedret,'photo') && (! x($vcard,'photo'))) - $vcard['photo'] = $feedret['photo']; - require_once('library/simplepie/simplepie.inc'); - $feed = new SimplePie(); - $xml = fetch_url($poll); - - logger('probe_url: fetch feed: ' . $poll . ' returns: ' . $xml, LOGGER_DATA); - $a = get_app(); - - logger('probe_url: scrape_feed: headers: ' . $a->get_curl_headers(), LOGGER_DATA); - - // Don't try and parse an empty string - $feed->set_raw_data(($xml) ? $xml : ''); - - $feed->init(); - if($feed->error()) { - logger('probe_url: scrape_feed: Error parsing XML: ' . $feed->error()); - $network = NETWORK_PHANTOM; - } - - if(! x($vcard,'photo')) - $vcard['photo'] = $feed->get_image_url(); - $author = $feed->get_author(); - - if($author) { - $vcard['fn'] = unxmlify(trim($author->get_name())); - if(! $vcard['fn']) - $vcard['fn'] = trim(unxmlify($author->get_email())); - if(strpos($vcard['fn'],'@') !== false) - $vcard['fn'] = substr($vcard['fn'],0,strpos($vcard['fn'],'@')); - - $email = unxmlify($author->get_email()); - if(! $profile && $author->get_link()) - $profile = trim(unxmlify($author->get_link())); - if(! $vcard['photo']) { - $rawtags = $feed->get_feed_tags( SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); - if($rawtags) { - $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]; - if((x($elems,'link')) && ($elems['link'][0]['attribs']['']['rel'] === 'photo')) - $vcard['photo'] = $elems['link'][0]['attribs']['']['href']; - } - } - // Fetch fullname via poco:displayName - $pocotags = $feed->get_feed_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); - if ($pocotags) { - $elems = $pocotags[0]['child']['http://portablecontacts.net/spec/1.0']; - if (isset($elems["displayName"])) - $vcard['fn'] = $elems["displayName"][0]["data"]; - if (isset($elems["preferredUsername"])) - $vcard['nick'] = $elems["preferredUsername"][0]["data"]; - } - } - else { - $item = $feed->get_item(0); - if($item) { - $author = $item->get_author(); - if($author) { - $vcard['fn'] = trim(unxmlify($author->get_name())); - if(! $vcard['fn']) - $vcard['fn'] = trim(unxmlify($author->get_email())); - if(strpos($vcard['fn'],'@') !== false) - $vcard['fn'] = substr($vcard['fn'],0,strpos($vcard['fn'],'@')); - $email = unxmlify($author->get_email()); - if(! $profile && $author->get_link()) - $profile = trim(unxmlify($author->get_link())); - } - if(! $vcard['photo']) { - $rawmedia = $item->get_item_tags('http://search.yahoo.com/mrss/','thumbnail'); - if($rawmedia && $rawmedia[0]['attribs']['']['url']) - $vcard['photo'] = unxmlify($rawmedia[0]['attribs']['']['url']); - } - if(! $vcard['photo']) { - $rawtags = $item->get_item_tags( SIMPLEPIE_NAMESPACE_ATOM_10, 'author'); - if($rawtags) { - $elems = $rawtags[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_10]; - if((x($elems,'link')) && ($elems['link'][0]['attribs']['']['rel'] === 'photo')) - $vcard['photo'] = $elems['link'][0]['attribs']['']['href']; - } - } - } - } - - // Workaround for misconfigured Friendica servers - if (($network == "") AND (strstr($url, "/profile/"))) { - $noscrape = str_replace("/profile/", "/noscrape/", $url); - $noscrapejson = fetch_url($noscrape); - if ($noscrapejson) { - - $network = NETWORK_DFRN; - - $poco = str_replace("/profile/", "/poco/", $url); - - $noscrapedata = json_decode($noscrapejson, true); - - if (isset($noscrapedata["addr"])) - $addr = $noscrapedata["addr"]; - - if (isset($noscrapedata["fn"])) - $vcard["fn"] = $noscrapedata["fn"]; - - if (isset($noscrapedata["key"])) - $pubkey = $noscrapedata["key"]; - - if (isset($noscrapedata["photo"])) - $vcard["photo"] = $noscrapedata["photo"]; - - if (isset($noscrapedata["dfrn-request"])) - $request = $noscrapedata["dfrn-request"]; - - if (isset($noscrapedata["dfrn-confirm"])) - $confirm = $noscrapedata["dfrn-confirm"]; - - if (isset($noscrapedata["dfrn-notify"])) - $notify = $noscrapedata["dfrn-notify"]; - - if (isset($noscrapedata["dfrn-poll"])) - $poll = $noscrapedata["dfrn-poll"]; - - } - } - - if((! $vcard['photo']) && strlen($email)) - $vcard['photo'] = avatar_img($email); - if($poll === $profile) - $lnk = $feed->get_permalink(); - if(isset($lnk) && strlen($lnk)) - $profile = $lnk; - - if(! $network) { - $network = NETWORK_FEED; - // If it is a feed, don't take the author name as feed name - unset($vcard['fn']); - } - if(! (x($vcard,'fn'))) - $vcard['fn'] = notags($feed->get_title()); - if(! (x($vcard,'fn'))) - $vcard['fn'] = notags($feed->get_description()); - - if(strpos($vcard['fn'],'Twitter / ') !== false) { - $vcard['fn'] = substr($vcard['fn'],strpos($vcard['fn'],'/')+1); - $vcard['fn'] = trim($vcard['fn']); - } - if(! x($vcard,'nick')) { - $vcard['nick'] = strtolower(notags(unxmlify($vcard['fn']))); - if(strpos($vcard['nick'],' ')) - $vcard['nick'] = trim(substr($vcard['nick'],0,strpos($vcard['nick'],' '))); - } - if(! $priority) - $priority = 2; - } - } - - if(! x($vcard,'photo')) { - $a = get_app(); - $vcard['photo'] = $a->get_baseurl() . '/images/person-175.jpg' ; - } - - if(! $profile) - $profile = $url; - - // No human could be associated with this link, use the URL as the contact name - - if(($network === NETWORK_FEED) && ($poll) && (! x($vcard,'fn'))) - $vcard['fn'] = $url; - - if (($notify != "") AND ($poll != "")) { - $baseurl = matching(normalise_link($notify), normalise_link($poll)); - - $baseurl2 = matching($baseurl, normalise_link($profile)); - if ($baseurl2 != "") - $baseurl = $baseurl2; - } - - if (($baseurl == "") AND ($notify != "")) - $baseurl = matching(normalise_link($profile), normalise_link($notify)); - - if (($baseurl == "") AND ($poll != "")) - $baseurl = matching(normalise_link($profile), normalise_link($poll)); - - $baseurl = rtrim($baseurl, "/"); - - if(strpos($url,'@') AND ($addr == "") AND ($network == NETWORK_DFRN)) - $addr = str_replace('acct:', '', $url); - - $vcard['fn'] = notags($vcard['fn']); - $vcard['nick'] = str_replace(' ','',notags($vcard['nick'])); - - $result['name'] = $vcard['fn']; - $result['nick'] = $vcard['nick']; - $result['url'] = $profile; - $result['addr'] = $addr; - $result['batch'] = $batch; - $result['notify'] = $notify; - $result['poll'] = $poll; - $result['request'] = $request; - $result['confirm'] = $confirm; - $result['poco'] = $poco; - $result['photo'] = $vcard['photo']; - $result['priority'] = $priority; - $result['network'] = $network; - $result['alias'] = $alias; - $result['pubkey'] = $pubkey; - $result['baseurl'] = $baseurl; - - logger('probe_url: ' . print_r($result,true), LOGGER_DEBUG); - - if ($level == 1) { - // Trying if it maybe a diaspora account - if (($result['network'] == NETWORK_FEED) OR ($result['addr'] == "")) { - require_once('include/bbcode.php'); - $address = GetProfileUsername($url, "", true); - $result2 = probe_url($address, $mode, ++$level); - if ($result2['network'] != "") - $result = $result2; - } - - // Maybe it's some non standard GNU Social installation (Single user, subfolder or no uri rewrite) - if (($result['network'] == NETWORK_FEED) AND ($result['baseurl'] != "") AND ($result['nick'] != "")) { - $addr = $result['nick'].'@'.str_replace("http://", "", $result['baseurl']); - $result2 = probe_url($addr, $mode, ++$level); - if (($result2['network'] != "") AND ($result2['network'] != NETWORK_FEED)) - $result = $result2; - } - } - - // Only store into the cache if the value seems to be valid - if ($result['network'] != NETWORK_PHANTOM) - Cache::set("probe_url:".$mode.":".$url,serialize($result), CACHE_DAY); - - return $result; -} - -function matching($part1, $part2) { - $len = min(strlen($part1), strlen($part2)); - - $match = ""; - $matching = true; - $i = 0; - while (($i <= $len) AND $matching) { - if (substr($part1, $i, 1) == substr($part2, $i, 1)) - $match .= substr($part1, $i, 1); - else - $matching = false; - - $i++; - } - return($match); + return $data; } diff --git a/include/Smilies.php b/include/Smilies.php new file mode 100644 index 0000000000..d67b92d8b0 --- /dev/null +++ b/include/Smilies.php @@ -0,0 +1,182 @@ + smilie shortcut + * 'icons' => icon in html + * + * @hook smilie ('texts' => smilies texts array, 'icons' => smilies html array) + */ + public static function get_list() { + + $texts = array( + '<3', + '</3', + '<\\3', + ':-)', + ';-)', + ':-(', + ':-P', + ':-p', + ':-"', + ':-"', + ':-x', + ':-X', + ':-D', + '8-|', + '8-O', + ':-O', + '\\o/', + 'o.O', + 'O.o', + 'o_O', + 'O_o', + ":'(", + ":-!", + ":-/", + ":-[", + "8-)", + ':beer', + ':homebrew', + ':coffee', + ':facepalm', + ':like', + ':dislike', + '~friendica', + 'red#', + 'red#matrix' + + ); + + $icons = array( + '<3', + '</3', + '<\\3', + ':-)', + ';-)', + ':-(', + ':-P', + ':-p', + ':-\', + ':-\', + ':-x', + ':-X', + ':-D', + '8-|', + '8-O', + ':-O', + '\\o/', + 'o.O', + 'O.o', + 'o_O', + 'O_o', + ':\'(', + ':-!', + ':-/', + ':-[', + '8-)', + ':beer', + ':homebrew', + ':coffee', + ':facepalm', + ':like', + ':dislike', + '~friendica ~friendica', + 'redred#matrix', + 'redred#matrixmatrix' + ); + + $params = array('texts' => $texts, 'icons' => $icons); + call_hooks('smilie', $params); + + return $params; + + } + + /** + * @brief Replaces text emoticons with graphical images + * + * It is expected that this function will be called using HTML text. + * We will escape text between HTML pre and code blocks from being + * processed. + * + * At a higher level, the bbcode [nosmile] tag can be used to prevent this + * function from being executed by the prepare_text() routine when preparing + * bbcode source for HTML display + * + * @param string $s + * @param boolean $sample + * + * @return string HML Output of the Smilie + */ + public static function replace($s, $sample = false) { + if(intval(get_config('system','no_smilies')) + || (local_user() && intval(get_pconfig(local_user(),'system','no_smilies')))) + return $s; + + $s = preg_replace_callback('/
(.*?)<\/pre>/ism','self::encode',$s);
+		$s = preg_replace_callback('/(.*?)<\/code>/ism','self::encode',$s);
+
+		$params = self::get_list();
+		$params['string'] = $s;
+
+		if($sample) {
+			$s = '
'; + for($x = 0; $x < count($params['texts']); $x ++) { + $s .= '
' . $params['texts'][$x] . '
' . $params['icons'][$x] . '
'; + } + } + else { + $params['string'] = preg_replace_callback('/<(3+)/','self::preg_heart',$params['string']); + $s = str_replace($params['texts'],$params['icons'],$params['string']); + } + + $s = preg_replace_callback('/
(.*?)<\/pre>/ism','self::decode',$s);
+		$s = preg_replace_callback('/(.*?)<\/code>/ism','self::decode',$s);
+
+		return $s;
+	}
+
+	private function encode($m) {
+		return(str_replace($m[1],base64url_encode($m[1]),$m[0]));
+	}
+
+	private function decode($m) {
+		return(str_replace($m[1],base64url_decode($m[1]),$m[0]));
+	}
+
+
+	/**
+	 * @brief expand <3333 to the correct number of hearts
+	 *
+	 * @param string $x
+	 * @return string HTML Output
+	 * 
+	 * @todo: Rework because it doesn't work correctly
+	 */
+	private function preg_heart($x) {
+		if(strlen($x[1]) == 1)
+			return $x[0];
+		$t = '';
+		for($cnt = 0; $cnt < strlen($x[1]); $cnt ++)
+			$t .= '<3';
+		$r =  str_replace($x[0],$t,$x[0]);
+		return $r;
+	}
+
+}
diff --git a/include/acl_selectors.php b/include/acl_selectors.php
index 4ef3d05ea3..f4b644d68f 100644
--- a/include/acl_selectors.php
+++ b/include/acl_selectors.php
@@ -1,13 +1,15 @@
 \r\n";
 
-	$r = q("SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d ORDER BY `name` ASC",
+	$r = q("SELECT `id`, `name` FROM `group` WHERE NOT `deleted` AND `uid` = %d ORDER BY `name` ASC",
 		intval(local_user())
 	);
 
@@ -31,8 +33,8 @@ function group_select($selname,$selclass,$preselected = false,$size = 4) {
 
 	call_hooks($a->module . '_pre_' . $selname, $arr);
 
-	if(count($r)) {
-		foreach($r as $rr) {
+	if (dbm::is_result($r)) {
+		foreach ($r as $rr) {
 			if((is_array($preselected)) && in_array($rr['id'], $preselected))
 				$selected = " selected=\"selected\" ";
 			else
@@ -63,20 +65,24 @@ function contact_selector($selname, $selclass, $preselected = false, $options) {
 	$exclude = false;
 	$size = 4;
 
-	if(is_array($options)) {
-		if(x($options,'size'))
+	if (is_array($options)) {
+		if (x($options,'size'))
 			$size = $options['size'];
 
-		if(x($options,'mutual_friends'))
+		if (x($options,'mutual_friends')) {
 			$mutual = true;
-		if(x($options,'single'))
+		}
+		if (x($options,'single')) {
 			$single = true;
-		if(x($options,'multiple'))
+		}
+		if (x($options,'multiple')) {
 			$single = false;
-		if(x($options,'exclude'))
+		}
+		if (x($options,'exclude')) {
 			$exclude = $options['exclude'];
+		}
 
-		if(x($options,'networks')) {
+		if (x($options,'networks')) {
 			switch($options['networks']) {
 				case 'DFRN_ONLY':
 					$networks = array(NETWORK_DFRN);
@@ -129,7 +135,7 @@ function contact_selector($selname, $selclass, $preselected = false, $options) {
 		$o .= "\r\n";
 
 	$r = q("SELECT `id`, `name`, `url`, `network` FROM `contact`
-		WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0 AND `notify` != ''
+		WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
 		$sql_extra
 		ORDER BY `name` ASC ",
 		intval(local_user())
@@ -218,17 +225,20 @@ function contact_select($selname, $selclass, $preselected = false, $size = 4, $p
 
 	$receiverlist = array();
 
-	if(count($r)) {
-		foreach($r as $rr) {
-			if((is_array($preselected)) && in_array($rr['id'], $preselected))
+	if (dbm::is_result($r)) {
+		foreach ($r as $rr) {
+			if ((is_array($preselected)) && in_array($rr['id'], $preselected)) {
 				$selected = " selected=\"selected\" ";
-			else
+			}
+			else {
 				$selected = '';
+			}
 
-			if($privmail)
+			if ($privmail) {
 				$trimmed = GetProfileUsername($rr['url'], $rr['name'], false);
-			else
+			} else {
 				$trimmed = mb_substr($rr['name'],0,20);
+			}
 
 			$receiverlist[] = $trimmed;
 
@@ -254,16 +264,22 @@ function fixacl(&$item) {
 
 function prune_deadguys($arr) {
 
-	if(! $arr)
+	if (! $arr) {
 		return $arr;
+	}
+
 	$str = dbesc(implode(',',$arr));
-	$r = q("select id from contact where id in ( " . $str . ") and blocked = 0 and pending = 0 and archive = 0 ");
-	if($r) {
+
+	$r = q("SELECT `id` FROM `contact` WHERE `id` IN ( " . $str . ") AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0 ");
+
+	if (dbm::is_result($r)) {
 		$ret = array();
-		foreach($r as $rr)
+		foreach ($r as $rr) {
 			$ret[] = intval($rr['id']);
+		}
 		return $ret;
 	}
+
 	return array();
 }
 
@@ -309,10 +325,10 @@ function populate_acl($user = null, $show_jotnets = false) {
 		$pubmail_enabled = false;
 
 		if(! $mail_disabled) {
-			$r = q("SELECT * FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1",
+			$r = q("SELECT `pubmail` FROM `mailacct` WHERE `uid` = %d AND `server` != '' LIMIT 1",
 				intval(local_user())
 			);
-			if(count($r)) {
+			if (dbm::is_result($r)) {
 				$mail_enabled = true;
 				if(intval($r[0]['pubmail']))
 					$pubmail_enabled = true;
@@ -356,7 +372,7 @@ function populate_acl($user = null, $show_jotnets = false) {
 
 }
 
-function construct_acl_data(&$a, $user) {
+function construct_acl_data(App $a, $user) {
 
 	// Get group and contact information for html ACL selector
 	$acl_data = acl_lookup($a, 'html');
@@ -388,18 +404,20 @@ function construct_acl_data(&$a, $user) {
 
 }
 
-function acl_lookup(&$a, $out_type = 'json') {
+function acl_lookup(App $a, $out_type = 'json') {
 
-	if(!local_user())
-		return "";
+	if (!local_user()) {
+		return '';
+	}
 
-	$start = (x($_REQUEST,'start')?$_REQUEST['start']:0);
-	$count = (x($_REQUEST,'count')?$_REQUEST['count']:100);
-	$search = (x($_REQUEST,'search')?$_REQUEST['search']:"");
-	$type = (x($_REQUEST,'type')?$_REQUEST['type']:"");
-	$conv_id = (x($_REQUEST,'conversation')?$_REQUEST['conversation']:null);
+	$start	=	(x($_REQUEST,'start')		? $_REQUEST['start']		: 0);
+	$count	=	(x($_REQUEST,'count')		? $_REQUEST['count']		: 100);
+	$search	 =	(x($_REQUEST,'search')		? $_REQUEST['search']		: "");
+	$type	=	(x($_REQUEST,'type')		? $_REQUEST['type']		: "");
+	$mode	=	(x($_REQUEST,'smode')		? $_REQUEST['smode']		: "");
+	$conv_id =	(x($_REQUEST,'conversation')	? $_REQUEST['conversation']	: null);
 
-	// For use with jquery.autocomplete for private mail completion
+	// For use with jquery.textcomplete for private mail completion
 
 	if(x($_REQUEST,'query') && strlen($_REQUEST['query'])) {
 		if(! $type)
@@ -407,7 +425,7 @@ function acl_lookup(&$a, $out_type = 'json') {
 		$search = $_REQUEST['query'];
 	}
 
-//	logger("Searching for ".$search." - type ".$type, LOGGER_DEBUG);
+	logger("Searching for ".$search." - type ".$type, LOGGER_DEBUG);
 
 	if ($search!=""){
 		$sql_extra = "AND `name` LIKE '%%".dbesc($search)."%%'";
@@ -428,10 +446,11 @@ function acl_lookup(&$a, $out_type = 'json') {
 
 	$sql_extra2 .= " ".unavailable_networks();
 
+	// autocomplete for editor mentions
 	if ($type=='' || $type=='c'){
 		$r = q("SELECT COUNT(*) AS c FROM `contact`
-				WHERE `uid` = %d AND `self` = 0
-				AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0
+				WHERE `uid` = %d AND NOT `self`
+				AND NOT `blocked` AND NOT `pending` AND NOT `archive`
 				AND `notify` != '' $sql_extra2" ,
 			intval(local_user())
 		);
@@ -442,8 +461,8 @@ function acl_lookup(&$a, $out_type = 'json') {
 		// autocomplete for Private Messages
 
 		$r = q("SELECT COUNT(*) AS c FROM `contact`
-				WHERE `uid` = %d AND `self` = 0
-				AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0
+				WHERE `uid` = %d AND NOT `self`
+				AND NOT `blocked` AND NOT `pending` AND NOT `archive`
 				AND `network` IN ('%s','%s','%s') $sql_extra2" ,
 			intval(local_user()),
 			dbesc(NETWORK_DFRN),
@@ -458,8 +477,8 @@ function acl_lookup(&$a, $out_type = 'json') {
 		// autocomplete for Contacts
 
 		$r = q("SELECT COUNT(*) AS c FROM `contact`
-				WHERE `uid` = %d AND `self` = 0
-				AND `pending` = 0 $sql_extra2" ,
+				WHERE `uid` = %d AND NOT `self`
+				AND NOT `pending` $sql_extra2" ,
 			intval(local_user())
 		);
 		$contact_count = (int)$r[0]['c'];
@@ -476,12 +495,14 @@ function acl_lookup(&$a, $out_type = 'json') {
 
 	if ($type=='' || $type=='g'){
 
-		$r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') as uids
-				FROM `group`,`group_member`
-				WHERE `group`.`deleted` = 0 AND `group`.`uid` = %d
-					AND `group_member`.`gid`=`group`.`id`
+		/// @todo We should cache this query.
+		// This can be done when we can delete cache entries via wildcard
+		$r = q("SELECT `group`.`id`, `group`.`name`, GROUP_CONCAT(DISTINCT `group_member`.`contact-id` SEPARATOR ',') AS uids
+				FROM `group`
+				INNER JOIN `group_member` ON `group_member`.`gid`=`group`.`id` AND `group_member`.`uid` = `group`.`uid`
+				WHERE NOT `group`.`deleted` AND `group`.`uid` = %d
 					$sql_extra
-				GROUP BY `group`.`id`
+				GROUP BY `group`.`name`
 				ORDER BY `group`.`name`
 				LIMIT %d,%d",
 			intval(local_user()),
@@ -503,10 +524,10 @@ function acl_lookup(&$a, $out_type = 'json') {
 		}
 	}
 
-	if ($type=='' || $type=='c'){
+	if ($type==''){
 
-		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, forum FROM `contact`
-			WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0 AND `notify` != ''
+		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `forum`, `prv` FROM `contact`
+			WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
 			AND NOT (`network` IN ('%s', '%s'))
 			$sql_extra2
 			ORDER BY `name` ASC ",
@@ -514,9 +535,20 @@ function acl_lookup(&$a, $out_type = 'json') {
 			dbesc(NETWORK_OSTATUS), dbesc(NETWORK_STATUSNET)
 		);
 	}
+	elseif ($type=='c'){
+
+		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `forum`, `prv` FROM `contact`
+			WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive` AND `notify` != ''
+			AND NOT (`network` IN ('%s'))
+			$sql_extra2
+			ORDER BY `name` ASC ",
+			intval(local_user()),
+			dbesc(NETWORK_STATUSNET)
+		);
+	}
 	elseif($type == 'm') {
 		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag` FROM `contact`
-			WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `pending` = 0 AND `archive` = 0
+			WHERE `uid` = %d AND NOT `self` AND NOT `blocked` AND NOT `pending` AND NOT `archive`
 			AND `network` IN ('%s','%s','%s')
 			$sql_extra2
 			ORDER BY `name` ASC ",
@@ -525,49 +557,52 @@ function acl_lookup(&$a, $out_type = 'json') {
 			dbesc(NETWORK_ZOT),
 			dbesc(NETWORK_DIASPORA)
 		);
-	}
-	elseif($type == 'a') {
-		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag` FROM `contact`
+	} elseif ($type == 'a') {
+		$r = q("SELECT `id`, `name`, `nick`, `micro`, `network`, `url`, `attag`, `forum`, `prv` FROM `contact`
 			WHERE `uid` = %d AND `pending` = 0
 			$sql_extra2
 			ORDER BY `name` ASC ",
 			intval(local_user())
 		);
-	}
-	else
-		$r = array();
-
-
-	if($type == 'm' || $type == 'a') {
-		$x = array();
-		$x['query'] = $search;
-		$x['photos'] = array();
-		$x['links'] = array();
-		$x['suggestions'] = array();
-		$x['data'] = array();
-		if(count($r)) {
-			foreach($r as $g) {
-				$x['photos'][] = proxy_url($g['micro'], false, PROXY_SIZE_MICRO);
-				$x['links'][] = $g['url'];
-				$x['suggestions'][] = htmlentities($g['name']);
-				$x['data'][] = intval($g['id']);
+	} elseif ($type == 'x') {
+		// autocomplete for global contact search (e.g. navbar search)
+		$r = navbar_complete($a);
+		$contacts = array();
+		if ($r) {
+			foreach ($r as $g) {
+				$contacts[] = array(
+					'photo'   => proxy_url($g['photo'], false, PROXY_SIZE_MICRO),
+					'name'    => $g['name'],
+					'nick'    => (x($g['addr']) ? $g['addr'] : $g['url']),
+					'network' => $g['network'],
+					'link'    => $g['url'],
+					'forum'   => (x($g['community']) ? 1 : 0),
+				);
 			}
 		}
-		echo json_encode($x);
+		$o = array(
+			'start' => $start,
+			'count' => $count,
+			'items' => $contacts,
+		);
+		echo json_encode($o);
 		killme();
+	} else {
+		$r = array();
 	}
 
-	if(count($r)) {
-		foreach($r as $g){
+
+	if (dbm::is_result($r)) {
+		foreach ($r as $g){
 			$contacts[] = array(
-				"type"  => "c",
-				"photo" => proxy_url($g['micro'], false, PROXY_SIZE_MICRO),
-				"name"  => htmlentities($g['name']),
-				"id"	=> intval($g['id']),
-				"network" => $g['network'],
-				"link" => $g['url'],
-				"nick" => htmlentities(($g['attag']) ? $g['attag'] : $g['nick']),
-				"forum" => $g['forum']
+				'type'    => 'c',
+				'photo'   => proxy_url($g['micro'], false, PROXY_SIZE_MICRO),
+				'name'    => htmlentities($g['name']),
+				'id'      => intval($g['id']),
+				'network' => $g['network'],
+				'link'    => $g['url'],
+				'nick'    => htmlentities(($g['attag']) ? $g['attag'] : $g['nick']),
+				'forum'   => ((x($g['forum']) || x($g['prv'])) ? 1 : 0),
 			);
 		}
 	}
@@ -580,14 +615,10 @@ function acl_lookup(&$a, $out_type = 'json') {
 		function _contact_link($i){ return dbesc($i['link']); }
 		$known_contacts = array_map(_contact_link, $contacts);
 		$unknow_contacts=array();
-		$r = q("select
-					`author-avatar`,`author-name`,`author-link`
-				from item where parent=%d
-				and (
-					`author-name` LIKE '%%%s%%' OR
-					`author-link` LIKE '%%%s%%'
-				) and
-				`author-link` NOT IN ('%s')
+		$r = q("SELECT `author-avatar`,`author-name`,`author-link`
+				FROM `item` WHERE `parent` = %d
+					AND (`author-name` LIKE '%%%s%%' OR `author-link` LIKE '%%%s%%')
+					AND `author-link` NOT IN ('%s')
 				GROUP BY `author-link`
 				ORDER BY `author-name` ASC
 				",
@@ -596,8 +627,8 @@ function acl_lookup(&$a, $out_type = 'json') {
 				dbesc($search),
 				implode("','", $known_contacts)
 		);
-		if (is_array($r) && count($r)){
-			foreach($r as $row) {
+		if (dbm::is_result($r)){
+			foreach ($r as $row) {
 				// nickname..
 				$up = parse_url($row['author-link']);
 				$nick = explode("/",$up['path']);
@@ -605,14 +636,14 @@ function acl_lookup(&$a, $out_type = 'json') {
 				$nick .= "@".$up['host'];
 				// /nickname
 				$unknow_contacts[] = array(
-					"type"  => "c",
-					"photo" => proxy_url($row['author-avatar'], false, PROXY_SIZE_MICRO),
-					"name"  => htmlentities($row['author-name']),
-					"id"	=> '',
-					"network" => "unknown",
-					"link" => $row['author-link'],
-					"nick" => htmlentities($nick),
-					"forum" => false
+					'type'    => 'c',
+					'photo'   => proxy_url($row['author-avatar'], false, PROXY_SIZE_MICRO),
+					'name'    => htmlentities($row['author-name']),
+					'id'      => '',
+					'network' => 'unknown',
+					'link'    => $row['author-link'],
+					'nick'    => htmlentities($nick),
+					'forum'   => false
 				);
 			}
 		}
@@ -621,26 +652,88 @@ function acl_lookup(&$a, $out_type = 'json') {
 		$tot += count($unknow_contacts);
 	}
 
+	$results = array(
+		'tot'      => $tot,
+		'start'    => $start,
+		'count'    => $count,
+		'groups'   => $groups,
+		'contacts' => $contacts,
+		'items'    => $items,
+		'type'     => $type,
+		'search'   => $search,
+	);
+
+	call_hooks('acl_lookup_end', $results);
+
 	if($out_type === 'html') {
 		$o = array(
-			'tot'		=> $tot,
-			'start'	=> $start,
-			'count'	=> $count,
-			'groups'	=> $groups,
-			'contacts'	=> $contacts,
+			'tot'      => $results['tot'],
+			'start'    => $results['start'],
+			'count'    => $results['count'],
+			'groups'   => $results['groups'],
+			'contacts' => $results['contacts'],
 		);
 		return $o;
 	}
 
 	$o = array(
-		'tot'	=> $tot,
-		'start' => $start,
-		'count'	=> $count,
-		'items'	=> $items,
+		'tot'   => $results['tot'],
+		'start' => $results['start'],
+		'count' => $results['count'],
+		'items' => $results['items'],
 	);
 
 	echo json_encode($o);
 
 	killme();
 }
+/**
+ * @brief Searching for global contacts for autocompletion
+ *
+ * @param App $a
+ * @return array with the search results
+ */
+function navbar_complete(App $a) {
 
+//	logger('navbar_complete');
+
+	if ((get_config('system','block_public')) && (! local_user()) && (! remote_user())) {
+		return;
+	}
+
+	// check if searching in the local global contact table is enabled
+	$localsearch = get_config('system','poco_local_search');
+
+	$search = $prefix.notags(trim($_REQUEST['search']));
+	$mode = $_REQUEST['smode'];
+
+	// don't search if search term has less than 2 characters
+	if (! $search || mb_strlen($search) < 2) {
+		return array();
+	}
+
+	if (substr($search,0,1) === '@') {
+		$search = substr($search,1);
+	}
+
+	if ($localsearch) {
+		$x = DirSearch::global_search_by_name($search, $mode);
+		return $x;
+	}
+
+	if (! $localsearch) {
+		$p = (($a->pager['page'] != 1) ? '&p=' . $a->pager['page'] : '');
+
+		$x = z_fetch_url(get_server().'/lsearch?f=' . $p .  '&search=' . urlencode($search));
+		if ($x['success']) {
+			$t = 0;
+			$j = json_decode($x['body'],true);
+			if ($j && $j['results']) {
+				return $j['results'];
+			}
+		}
+	}
+
+	/// @TODO Not needed here?
+	return;
+}
diff --git a/include/api.php b/include/api.php
index 531b66814f..d7fa1d5875 100644
--- a/include/api.php
+++ b/include/api.php
@@ -1,42 +1,71 @@
 ".
+	 * Some clients doesn't send a source param, we support ones we know
+	 * (only Twidere, atm)
+	 *
+	 * @return string
+	 * 		Client source name, default to "api" if unset/unknown
+	 */
 	function api_source() {
 		if (requestdata('source'))
 			return (requestdata('source'));
@@ -50,26 +79,64 @@
 		return ("api");
 	}
 
+	/**
+	 * @brief Format date for API
+	 *
+	 * @param string $str Source date, as UTC
+	 * @return string Date in UTC formatted as "D M d H:i:s +0000 Y"
+	 */
 	function api_date($str){
 		//Wed May 23 06:01:13 +0000 2007
 		return datetime_convert('UTC', 'UTC', $str, "D M d H:i:s +0000 Y" );
 	}
 
-
-	function api_register_func($path, $func, $auth=false){
+	/**
+	 * @brief Register API endpoint
+	 *
+	 * Register a function to be the endpont for defined API path.
+	 *
+	 * @param string $path API URL path, relative to App::get_baseurl()
+	 * @param string $func Function name to call on path request
+	 * @param bool $auth API need logged user
+	 * @param string $method
+	 * 	HTTP method reqiured to call this endpoint.
+	 * 	One of API_METHOD_ANY, API_METHOD_GET, API_METHOD_POST.
+	 *  Default to API_METHOD_ANY
+	 */
+	function api_register_func($path, $func, $auth=false, $method=API_METHOD_ANY){
 		global $API;
-		$API[$path] = array('func'=>$func, 'auth'=>$auth);
+		$API[$path] = array(
+			'func'=>$func,
+			'auth'=>$auth,
+			'method'=> $method
+		);
 
 		// Workaround for hotot
 		$path = str_replace("api/", "api/1.1/", $path);
-		$API[$path] = array('func'=>$func, 'auth'=>$auth);
+		$API[$path] = array(
+			'func'=>$func,
+			'auth'=>$auth,
+			'method'=> $method
+		);
 	}
 
 	/**
-	 * Simple HTTP Login
+	 * @brief Login API user
+	 *
+	 * Log in user via OAuth1 or Simple HTTP Auth.
+	 * Simple Auth allow username in form of 
user@server
, ignoring server part + * + * @param App $a + * @hook 'authenticate' + * array $addon_auth + * 'username' => username from login form + * 'password' => password from login form + * 'authenticated' => return status, + * 'user_record' => return authenticated user record + * @hook 'logged_in' + * array $user logged user record */ - - function api_login(&$a){ + function api_login(App $a){ // login with oauth try{ $oauth = new FKOAuth1(); @@ -81,8 +148,7 @@ } echo __file__.__line__.__function__."
"; var_dump($consumer, $token); die();
 		}catch(Exception $e){
-			logger(__file__.__line__.__function__."\n".$e);
-			//die(__file__.__line__.__function__."
".$e); die();
+			logger($e);
 		}
 
 
@@ -100,10 +166,7 @@
 		if (!isset($_SERVER['PHP_AUTH_USER'])) {
 			logger('API_login: ' . print_r($_SERVER,true), LOGGER_DEBUG);
 			header('WWW-Authenticate: Basic realm="Friendica"');
-			header('HTTP/1.0 401 Unauthorized');
-			die((api_error($a, 'json', "This api requires login")));
-
-			//die('This api requires login');
+			throw new UnauthorizedException("This API requires login");
 		}
 
 		$user = $_SERVER['PHP_AUTH_USER'];
@@ -142,139 +205,219 @@
 		else {
 			// process normal login request
 
-			$r = q("SELECT * FROM `user` WHERE ( `email` = '%s' OR `nickname` = '%s' )
-				AND `password` = '%s' AND `blocked` = 0 AND `account_expired` = 0 AND `account_removed` = 0 AND `verified` = 1 LIMIT 1",
+			$r = q("SELECT * FROM `user` WHERE (`email` = '%s' OR `nickname` = '%s')
+				AND `password` = '%s' AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` LIMIT 1",
 				dbesc(trim($user)),
 				dbesc(trim($user)),
 				dbesc($encrypted)
 			);
-			if(count($r))
+			if (dbm::is_result($r))
 				$record = $r[0];
 		}
 
 		if((! $record) || (! count($record))) {
 			logger('API_login failure: ' . print_r($_SERVER,true), LOGGER_DEBUG);
 			header('WWW-Authenticate: Basic realm="Friendica"');
-			header('HTTP/1.0 401 Unauthorized');
-			die('This api requires login');
+			#header('HTTP/1.0 401 Unauthorized');
+			#die('This api requires login');
+			throw new UnauthorizedException("This API requires login");
 		}
 
-		authenticate_success($record); $_SESSION["allow_api"] = true;
+		authenticate_success($record);
+
+		$_SESSION["allow_api"] = true;
 
 		call_hooks('logged_in', $a->user);
 
 	}
 
-	/**************************
-	 *  MAIN API ENTRY POINT  *
-	 **************************/
-	function api_call(&$a){
-		GLOBAL $API, $called_api;
+	/**
+	 * @brief Check HTTP method of called API
+	 *
+	 * API endpoints can define which HTTP method to accept when called.
+	 * This function check the current HTTP method agains endpoint
+	 * registered method.
+	 *
+	 * @param string $method Required methods, uppercase, separated by comma
+	 * @return bool
+	 */
+	 function api_check_method($method) {
+		if ($method=="*") return True;
+		return strpos($method, $_SERVER['REQUEST_METHOD']) !== false;
+	 }
+
+	/**
+	 * @brief Main API entry point
+	 *
+	 * Authenticate user, call registered API function, set HTTP headers
+	 *
+	 * @param App $a
+	 * @return string API call result
+	 */
+	function api_call(App $a){
+		global $API, $called_api;
 
-		// preset
 		$type="json";
-		foreach ($API as $p=>$info){
-			if (strpos($a->query_string, $p)===0){
-				$called_api= explode("/",$p);
-				//unset($_SERVER['PHP_AUTH_USER']);
-				if ($info['auth']===true && api_user()===false) {
-						api_login($a);
+		if (strpos($a->query_string, ".xml")>0) $type="xml";
+		if (strpos($a->query_string, ".json")>0) $type="json";
+		if (strpos($a->query_string, ".rss")>0) $type="rss";
+		if (strpos($a->query_string, ".atom")>0) $type="atom";
+		try {
+			foreach ($API as $p=>$info){
+				if (strpos($a->query_string, $p)===0){
+					if (!api_check_method($info['method'])){
+						throw new MethodNotAllowedException();
+					}
+
+					$called_api= explode("/",$p);
+					//unset($_SERVER['PHP_AUTH_USER']);
+					if ($info['auth']===true && api_user()===false) {
+							api_login($a);
+					}
+
+					logger('API call for ' . $a->user['username'] . ': ' . $a->query_string);
+					logger('API parameters: ' . print_r($_REQUEST,true));
+
+					$stamp =  microtime(true);
+					$r = call_user_func($info['func'], $type);
+					$duration = (float)(microtime(true)-$stamp);
+					logger("API call duration: ".round($duration, 2)."\t".$a->query_string, LOGGER_DEBUG);
+
+					if (get_config("system", "profiler")) {
+						$duration = microtime(true)-$a->performance["start"];
+
+						logger(parse_url($a->query_string, PHP_URL_PATH).": ".sprintf("Database: %s/%s, Network: %s, I/O: %s, Other: %s, Total: %s",
+							round($a->performance["database"] - $a->performance["database_write"], 3),
+							round($a->performance["database_write"], 3),
+							round($a->performance["network"], 2),
+							round($a->performance["file"], 2),
+							round($duration - ($a->performance["database"] + $a->performance["network"]
+								+ $a->performance["file"]), 2),
+							round($duration, 2)),
+							LOGGER_DEBUG);
+
+						if (get_config("rendertime", "callstack")) {
+							$o = "Database Read:\n";
+							foreach ($a->callstack["database"] AS $func => $time) {
+								$time = round($time, 3);
+								if ($time > 0)
+									$o .= $func.": ".$time."\n";
+							}
+							$o .= "\nDatabase Write:\n";
+							foreach ($a->callstack["database_write"] AS $func => $time) {
+								$time = round($time, 3);
+								if ($time > 0)
+									$o .= $func.": ".$time."\n";
+							}
+
+							$o .= "\nNetwork:\n";
+							foreach ($a->callstack["network"] AS $func => $time) {
+								$time = round($time, 3);
+								if ($time > 0)
+									$o .= $func.": ".$time."\n";
+							}
+							logger($o, LOGGER_DEBUG);
+						}
+					}
+
+
+					if ($r===false) {
+						// api function returned false withour throw an
+						// exception. This should not happend, throw a 500
+						throw new InternalServerErrorException();
+					}
+
+					switch($type){
+						case "xml":
+							header ("Content-Type: text/xml");
+							return $r;
+							break;
+						case "json":
+							header ("Content-Type: application/json");
+							foreach($r as $rr)
+								$json = json_encode($rr);
+								if ($_GET['callback'])
+									$json = $_GET['callback']."(".$json.")";
+								return $json;
+							break;
+						case "rss":
+							header ("Content-Type: application/rss+xml");
+							return ''."\n".$r;
+							break;
+						case "atom":
+							header ("Content-Type: application/atom+xml");
+							return ''."\n".$r;
+							break;
+
+					}
 				}
-
-				load_contact_links(api_user());
-
-				logger('API call for ' . $a->user['username'] . ': ' . $a->query_string);
-				logger('API parameters: ' . print_r($_REQUEST,true));
-				$type="json";
-				if (strpos($a->query_string, ".xml")>0) $type="xml";
-				if (strpos($a->query_string, ".json")>0) $type="json";
-				if (strpos($a->query_string, ".rss")>0) $type="rss";
-				if (strpos($a->query_string, ".atom")>0) $type="atom";
-				if (strpos($a->query_string, ".as")>0) $type="as";
-
-				$stamp =  microtime(true);
-				$r = call_user_func($info['func'], $a, $type);
-				$duration = (float)(microtime(true)-$stamp);
-				logger("API call duration: ".round($duration, 2)."\t".$a->query_string, LOGGER_DEBUG);
-
-				if ($r===false) return;
-
-				switch($type){
-					case "xml":
-						$r = mb_convert_encoding($r, "UTF-8",mb_detect_encoding($r));
-						header ("Content-Type: text/xml");
-						return ''."\n".$r;
-						break;
-					case "json":
-						header ("Content-Type: application/json");
-						foreach($r as $rr)
-							$json = json_encode($rr);
-							if ($_GET['callback'])
-								$json = $_GET['callback']."(".$json.")";
-							return $json;
-						break;
-					case "rss":
-						header ("Content-Type: application/rss+xml");
-						return ''."\n".$r;
-						break;
-					case "atom":
-						header ("Content-Type: application/atom+xml");
-						return ''."\n".$r;
-						break;
-					case "as":
-						//header ("Content-Type: application/json");
-						//foreach($r as $rr)
-						//	return json_encode($rr);
-						return json_encode($r);
-						break;
-
-				}
-				//echo "
"; var_dump($r); die();
 			}
+			throw new NotImplementedException();
+		} catch (HTTPException $e) {
+			header("HTTP/1.1 {$e->httpcode} {$e->httpdesc}");
+			return api_error($type, $e);
 		}
-		header("HTTP/1.1 404 Not Found");
-		logger('API call not implemented: '.$a->query_string." - ".print_r($_REQUEST,true));
-		return(api_error($a, $type, "not implemented"));
-
 	}
 
-	function api_error(&$a, $type, $error) {
+	/**
+	 * @brief Format API error string
+	 *
+	 * @param string $type Return type (xml, json, rss, as)
+	 * @param HTTPException $error Error object
+	 * @return strin error message formatted as $type
+	 */
+	function api_error($type, $e) {
+
+		$a = get_app();
+
+		$error = ($e->getMessage()!==""?$e->getMessage():$e->httpdesc);
 		# TODO:  https://dev.twitter.com/overview/api/response-codes
-		$r = "".$error."".$a->query_string."";
+
+		$error = array("error" => $error,
+				"code" => $e->httpcode." ".$e->httpdesc,
+				"request" => $a->query_string);
+
+		$ret = api_format_data('status', $type, array('status' => $error));
+
 		switch($type){
 			case "xml":
 				header ("Content-Type: text/xml");
-				return ''."\n".$r;
+				return $ret;
 				break;
 			case "json":
 				header ("Content-Type: application/json");
-				return json_encode(array('error' => $error, 'request' => $a->query_string));
+				return json_encode($ret);
 				break;
 			case "rss":
 				header ("Content-Type: application/rss+xml");
-				return ''."\n".$r;
+				return $ret;
 				break;
 			case "atom":
 				header ("Content-Type: application/atom+xml");
-				return ''."\n".$r;
+				return $ret;
 				break;
 		}
 	}
 
 	/**
-	 * RSS extra info
+	 * @brief Set values for RSS template
+	 *
+	 * @param App $a
+	 * @param array $arr Array to be passed to template
+	 * @param array $user_info
+	 * @return array
 	 */
-	function api_rss_extra(&$a, $arr, $user_info){
+	function api_rss_extra(App $a, $arr, $user_info){
 		if (is_null($user_info)) $user_info = api_get_user($a);
 		$arr['$user'] = $user_info;
 		$arr['$rss'] = array(
-			'alternate' => $user_info['url'],
-			'self' => $a->get_baseurl(). "/". $a->query_string,
-			'base' => $a->get_baseurl(),
-			'updated' => api_date(null),
+			'alternate'    => $user_info['url'],
+			'self'         => App::get_baseurl(). "/". $a->query_string,
+			'base'         => App::get_baseurl(),
+			'updated'      => api_date(null),
 			'atom_updated' => datetime_convert('UTC','UTC','now',ATOM_TIME),
-			'language' => $user_info['language'],
-			'logo'	=> $a->get_baseurl()."/images/friendica-32.png",
+			'language'     => $user_info['language'],
+			'logo'         => App::get_baseurl()."/images/friendica-32.png",
 		);
 
 		return $arr;
@@ -282,10 +425,14 @@
 
 
 	/**
-	 * Unique contact to contact url.
+	 * @brief Unique contact to contact url.
+	 *
+	 * @param int $id Contact id
+	 * @return bool|string
+	 * 		Contact url or False if contact id is unknown
 	 */
 	function api_unique_id_to_url($id){
-		$r = q("SELECT `url` FROM `unique_contacts` WHERE `id`=%d LIMIT 1",
+		$r = q("SELECT `url` FROM `contact` WHERE `uid` = 0 AND `id` = %d LIMIT 1",
 			intval($id));
 		if ($r)
 			return ($r[0]["url"]);
@@ -294,9 +441,13 @@
 	}
 
 	/**
-	 * Returns user info array.
+	 * @brief Get user info array.
+	 *
+	 * @param Api $a
+	 * @param int|string $contact_id Contact ID or URL
+	 * @param string $type Return type (for errors)
 	 */
-	function api_get_user(&$a, $contact_id = Null, $type = "json"){
+	function api_get_user(App $a, $contact_id = Null, $type = "json"){
 		global $called_api;
 		$user = null;
 		$extra_query = "";
@@ -313,12 +464,12 @@
 			if (api_user()!==false)  $extra_query .= "AND `contact`.`uid`=".intval(api_user());
 		}
 
-		// Searching for unique contact id
+		// Searching for contact id with uid = 0
 		if(!is_null($contact_id) AND (intval($contact_id) != 0)){
 			$user = dbesc(api_unique_id_to_url($contact_id));
 
 			if ($user == "")
-				die(api_error($a, $type, t("User not found.")));
+				throw new BadRequestException("User not found.");
 
 			$url = $user;
 			$extra_query = "AND `contact`.`nurl` = '%s' ";
@@ -329,7 +480,7 @@
 			$user = dbesc(api_unique_id_to_url($_GET['user_id']));
 
 			if ($user == "")
-				die(api_error($a, $type, t("User not found.")));
+				throw new BadRequestException("User not found.");
 
 			$url = $user;
 			$extra_query = "AND `contact`.`nurl` = '%s' ";
@@ -366,10 +517,11 @@
 
 		if (!$user) {
 			if (api_user()===false) {
-				api_login($a); return False;
+				api_login($a);
+				return False;
 			} else {
 				$user = $_SESSION['uid'];
-				$extra_query = "AND `contact`.`uid` = %d AND `contact`.`self` = 1 ";
+				$extra_query = "AND `contact`.`uid` = %d AND `contact`.`self` ";
 			}
 
 		}
@@ -385,16 +537,16 @@
 		// Selecting the id by priority, friendica first
 		api_best_nickname($uinfo);
 
-		// if the contact wasn't found, fetch it from the unique contacts
+		// if the contact wasn't found, fetch it from the contacts with uid = 0
 		if (count($uinfo)==0) {
 			$r = array();
 
 			if ($url != "")
-				$r = q("SELECT * FROM `unique_contacts` WHERE `url`='%s' LIMIT 1", $url);
-			elseif ($nick != "")
-				$r = q("SELECT * FROM `unique_contacts` WHERE `nick`='%s' LIMIT 1", $nick);
+				$r = q("SELECT * FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s' LIMIT 1", dbesc(normalise_link($url)));
 
 			if ($r) {
+				$network_name = network_to_name($r[0]['network'], $r[0]['url']);
+
 				// If no nick where given, extract it from the address
 				if (($r[0]['nick'] == "") OR ($r[0]['name'] == $r[0]['nick']))
 					$r[0]['nick'] = api_get_nick($r[0]["url"]);
@@ -404,14 +556,16 @@
 					'id_str' => (string) $r[0]["id"],
 					'name' => $r[0]["name"],
 					'screen_name' => (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']),
-					'location' => NULL,
-					'description' => NULL,
+					'location' => ($r[0]["location"] != "") ? $r[0]["location"] : $network_name,
+					'description' => $r[0]["about"],
+					'profile_image_url' => $r[0]["micro"],
+					'profile_image_url_https' => $r[0]["micro"],
 					'url' => $r[0]["url"],
 					'protected' => false,
 					'followers_count' => 0,
 					'friends_count' => 0,
 					'listed_count' => 0,
-					'created_at' => api_date(0),
+					'created_at' => api_date($r[0]["created"]),
 					'favourites_count' => 0,
 					'utc_offset' => 0,
 					'time_zone' => 'UTC',
@@ -422,27 +576,28 @@
 					'contributors_enabled' => false,
 					'is_translator' => false,
 					'is_translation_enabled' => false,
-					'profile_image_url' => $r[0]["avatar"],
-					'profile_image_url_https' => $r[0]["avatar"],
 					'following' => false,
 					'follow_request_sent' => false,
-					'notifications' => false,
 					'statusnet_blocking' => false,
 					'notifications' => false,
 					'statusnet_profile_url' => $r[0]["url"],
 					'uid' => 0,
-					'cid' => 0,
+					'cid' => get_contact($r[0]["url"], api_user(), true),
 					'self' => 0,
-					'network' => '',
+					'network' => $r[0]["network"],
 				);
 
 				return $ret;
-			} else
-				die(api_error($a, $type, t("User not found.")));
-
+			} else {
+				throw new BadRequestException("User not found.");
+			}
 		}
 
 		if($uinfo[0]['self']) {
+
+			if ($uinfo[0]['network'] == "")
+				$uinfo[0]['network'] = NETWORK_DFRN;
+
 			$usr = q("select * from user where uid = %d limit 1",
 				intval(api_user())
 			);
@@ -450,28 +605,28 @@
 				intval(api_user())
 			);
 
-			//AND `allow_cid`='' AND `allow_gid`='' AND `deny_cid`='' AND `deny_gid`=''",
+			// Counting is deactivated by now, due to performance issues
 			// count public wall messages
-			$r = q("SELECT count(*) as `count` FROM `item`
-					WHERE  `uid` = %d
-					AND `type`='wall'",
-					intval($uinfo[0]['uid'])
-			);
-			$countitms = $r[0]['count'];
+			//$r = q("SELECT COUNT(*) as `count` FROM `item` WHERE `uid` = %d AND `wall`",
+			//		intval($uinfo[0]['uid'])
+			//);
+			//$countitms = $r[0]['count'];
+			$countitms = 0;
+		} else {
+			// Counting is deactivated by now, due to performance issues
+			//$r = q("SELECT count(*) as `count` FROM `item`
+			//		WHERE  `contact-id` = %d",
+			//		intval($uinfo[0]['id'])
+			//);
+			//$countitms = $r[0]['count'];
+			$countitms = 0;
 		}
-		else {
-			//AND `allow_cid`='' AND `allow_gid`='' AND `deny_cid`='' AND `deny_gid`=''",
-			$r = q("SELECT count(*) as `count` FROM `item`
-					WHERE  `contact-id` = %d",
-					intval($uinfo[0]['id'])
-			);
-			$countitms = $r[0]['count'];
-		}
-
+/*
+		// Counting is deactivated by now, due to performance issues
 		// count friends
 		$r = q("SELECT count(*) as `count` FROM `contact`
 				WHERE  `uid` = %d AND `rel` IN ( %d, %d )
-				AND `self`=0 AND `blocked`=0 AND `pending`=0 AND `hidden`=0",
+				AND `self`=0 AND NOT `blocked` AND NOT `pending` AND `hidden`=0",
 				intval($uinfo[0]['uid']),
 				intval(CONTACT_IS_SHARING),
 				intval(CONTACT_IS_FRIEND)
@@ -480,7 +635,7 @@
 
 		$r = q("SELECT count(*) as `count` FROM `contact`
 				WHERE  `uid` = %d AND `rel` IN ( %d, %d )
-				AND `self`=0 AND `blocked`=0 AND `pending`=0 AND `hidden`=0",
+				AND `self`=0 AND NOT `blocked` AND NOT `pending` AND `hidden`=0",
 				intval($uinfo[0]['uid']),
 				intval(CONTACT_IS_FOLLOWER),
 				intval(CONTACT_IS_FRIEND)
@@ -498,28 +653,23 @@
 			$countfollowers = 0;
 			$starred = 0;
 		}
+*/
+		$countfriends = 0;
+		$countfollowers = 0;
+		$starred = 0;
 
 		// Add a nick if it isn't present there
 		if (($uinfo[0]['nick'] == "") OR ($uinfo[0]['name'] == $uinfo[0]['nick'])) {
 			$uinfo[0]['nick'] = api_get_nick($uinfo[0]["url"]);
 		}
 
-		// Fetching unique id
-		$r = q("SELECT id FROM `unique_contacts` WHERE `url`='%s' LIMIT 1", dbesc(normalise_link($uinfo[0]['url'])));
-
-		// If not there, then add it
-		if (count($r) == 0) {
-			q("INSERT INTO `unique_contacts` (`url`, `name`, `nick`, `avatar`) VALUES ('%s', '%s', '%s', '%s')",
-				dbesc(normalise_link($uinfo[0]['url'])), dbesc($uinfo[0]['name']),dbesc($uinfo[0]['nick']), dbesc($uinfo[0]['micro']));
-
-			$r = q("SELECT `id` FROM `unique_contacts` WHERE `url`='%s' LIMIT 1", dbesc(normalise_link($uinfo[0]['url'])));
-		}
-
 		$network_name = network_to_name($uinfo[0]['network'], $uinfo[0]['url']);
 
+		$pcontact_id  = get_contact($uinfo[0]['url'], 0, true);
+
 		$ret = Array(
-			'id' => intval($r[0]['id']),
-			'id_str' => (string) intval($r[0]['id']),
+			'id' => intval($pcontact_id),
+			'id_str' => (string) intval($pcontact_id),
 			'name' => (($uinfo[0]['name']) ? $uinfo[0]['name'] : $uinfo[0]['nick']),
 			'screen_name' => (($uinfo[0]['nick']) ? $uinfo[0]['nick'] : $uinfo[0]['name']),
 			'location' => ($usr) ? $usr[0]['default-location'] : $network_name,
@@ -530,16 +680,23 @@
 			'protected' => false,
 			'followers_count' => intval($countfollowers),
 			'friends_count' => intval($countfriends),
+			'listed_count' => 0,
 			'created_at' => api_date($uinfo[0]['created']),
 			'favourites_count' => intval($starred),
 			'utc_offset' => "0",
 			'time_zone' => 'UTC',
-			'statuses_count' => intval($countitms),
-			'following' => (($uinfo[0]['rel'] == CONTACT_IS_FOLLOWER) OR ($uinfo[0]['rel'] == CONTACT_IS_FRIEND)),
+			'geo_enabled' => false,
 			'verified' => true,
+			'statuses_count' => intval($countitms),
+			'lang' => '',
+			'contributors_enabled' => false,
+			'is_translator' => false,
+			'is_translation_enabled' => false,
+			'following' => (($uinfo[0]['rel'] == CONTACT_IS_FOLLOWER) OR ($uinfo[0]['rel'] == CONTACT_IS_FRIEND)),
+			'follow_request_sent' => false,
 			'statusnet_blocking' => false,
 			'notifications' => false,
-			//'statusnet_profile_url' => $a->get_baseurl()."/contacts/".$uinfo[0]['cid'],
+			//'statusnet_profile_url' => App::get_baseurl()."/contacts/".$uinfo[0]['cid'],
 			'statusnet_profile_url' => $uinfo[0]['url'],
 			'uid' => intval($uinfo[0]['uid']),
 			'cid' => intval($uinfo[0]['cid']),
@@ -551,54 +708,16 @@
 
 	}
 
-	function api_item_get_user(&$a, $item) {
+	/**
+	 * @brief return api-formatted array for item's author and owner
+	 *
+	 * @param App $a
+	 * @param array $item : item from db
+	 * @return array(array:author, array:owner)
+	 */
+	function api_item_get_user(App $a, $item) {
 
-		$author = q("SELECT * FROM `unique_contacts` WHERE `url`='%s' LIMIT 1",
-			dbesc(normalise_link($item['author-link'])));
-
-		if (count($author) == 0) {
-			q("INSERT INTO `unique_contacts` (`url`, `name`, `avatar`) VALUES ('%s', '%s', '%s')",
-				dbesc(normalise_link($item["author-link"])), dbesc($item["author-name"]), dbesc($item["author-avatar"]));
-
-			$author = q("SELECT `id` FROM `unique_contacts` WHERE `url`='%s' LIMIT 1",
-				dbesc(normalise_link($item['author-link'])));
-		} else if ($item["author-link"].$item["author-name"] != $author[0]["url"].$author[0]["name"]) {
-			$r = q("SELECT `id` FROM `unique_contacts` WHERE `name` = '%s' AND `avatar` = '%s' AND url = '%s'",
-				dbesc($item["author-name"]), dbesc($item["author-avatar"]),
-				dbesc(normalise_link($item["author-link"])));
-
-			if (!$r)
-				q("UPDATE `unique_contacts` SET `name` = '%s', `avatar` = '%s' WHERE `url` = '%s'",
-					dbesc($item["author-name"]), dbesc($item["author-avatar"]),
-					dbesc(normalise_link($item["author-link"])));
-		}
-
-		$owner = q("SELECT `id` FROM `unique_contacts` WHERE `url`='%s' LIMIT 1",
-			dbesc(normalise_link($item['owner-link'])));
-
-		if (count($owner) == 0) {
-			q("INSERT INTO `unique_contacts` (`url`, `name`, `avatar`) VALUES ('%s', '%s', '%s')",
-				dbesc(normalise_link($item["owner-link"])), dbesc($item["owner-name"]), dbesc($item["owner-avatar"]));
-
-			$owner = q("SELECT `id` FROM `unique_contacts` WHERE `url`='%s' LIMIT 1",
-				dbesc(normalise_link($item['owner-link'])));
-		} else if ($item["owner-link"].$item["owner-name"] != $owner[0]["url"].$owner[0]["name"]) {
-			$r = q("SELECT `id` FROM `unique_contacts` WHERE `name` = '%s' AND `avatar` = '%s' AND url = '%s'",
-				dbesc($item["owner-name"]), dbesc($item["owner-avatar"]),
-				dbesc(normalise_link($item["owner-link"])));
-
-			if (!$r)
-				q("UPDATE `unique_contacts` SET `name` = '%s', `avatar` = '%s' WHERE `url` = '%s'",
-					dbesc($item["owner-name"]), dbesc($item["owner-avatar"]),
-					dbesc(normalise_link($item["owner-link"])));
-		}
-
-		// Comments in threads may appear as wall-to-wall postings.
-		// So only take the owner at the top posting.
-		if ($item["id"] == $item["parent"])
-			$status_user = api_get_user($a,$item["owner-link"]);
-		else
-			$status_user = api_get_user($a,$item["author-link"]);
+		$status_user = api_get_user($a, $item["author-link"]);
 
 		$status_user["protected"] = (($item["allow_cid"] != "") OR
 						($item["allow_gid"] != "") OR
@@ -606,14 +725,110 @@
 						($item["deny_gid"] != "") OR
 						$item["private"]);
 
-		return ($status_user);
+		$owner_user = api_get_user($a, $item["owner-link"]);
+
+		return (array($status_user, $owner_user));
 	}
 
+	/**
+	 * @brief walks recursively through an array with the possibility to change value and key
+	 *
+	 * @param array $array The array to walk through
+	 * @param string $callback The callback function
+	 *
+	 * @return array the transformed array
+	 */
+	function api_walk_recursive(array &$array, callable $callback) {
+
+		$new_array = array();
+
+		foreach ($array as $k => $v) {
+			if (is_array($v)) {
+				if ($callback($v, $k))
+					$new_array[$k] = api_walk_recursive($v, $callback);
+			} else {
+				if ($callback($v, $k))
+					$new_array[$k] = $v;
+			}
+		}
+		$array = $new_array;
+
+		return $array;
+	}
 
 	/**
-	 *  load api $templatename for $type and replace $data array
+	 * @brief Callback function to transform the array in an array that can be transformed in a XML file
+	 *
+	 * @param variant $item Array item value
+	 * @param string $key Array key
+	 *
+	 * @return boolean Should the array item be deleted?
 	 */
-	function api_apply_template($templatename, $type, $data){
+	function api_reformat_xml(&$item, &$key) {
+		if (is_bool($item))
+			$item = ($item ? "true" : "false");
+
+		if (substr($key, 0, 10) == "statusnet_")
+			$key = "statusnet:".substr($key, 10);
+		elseif (substr($key, 0, 10) == "friendica_")
+			$key = "friendica:".substr($key, 10);
+		//else
+		//	$key = "default:".$key;
+
+		return true;
+	}
+
+	/**
+	 * @brief Creates the XML from a JSON style array
+	 *
+	 * @param array $data JSON style array
+	 * @param string $root_element Name of the root element
+	 *
+	 * @return string The XML data
+	 */
+	function api_create_xml($data, $root_element) {
+		$childname = key($data);
+		$data2 = array_pop($data);
+		$key = key($data2);
+
+		$namespaces = array("" => "http://api.twitter.com",
+					"statusnet" => "http://status.net/schema/api/1/",
+					"friendica" => "http://friendi.ca/schema/api/1/",
+					"georss" => "http://www.georss.org/georss");
+
+		/// @todo Auto detection of needed namespaces
+		if (in_array($root_element, array("ok", "hash", "config", "version", "ids", "notes", "photos")))
+			$namespaces = array();
+
+		if (is_array($data2))
+			api_walk_recursive($data2, "api_reformat_xml");
+
+		if ($key == "0") {
+			$data4 = array();
+			$i = 1;
+
+			foreach ($data2 AS $item)
+				$data4[$i++.":".$childname] = $item;
+
+			$data2 = $data4;
+		}
+
+		$data3 = array($root_element => $data2);
+
+		$ret = xml::from_array($data3, $xml, false, $namespaces);
+		return $ret;
+	}
+
+	/**
+	 * @brief Formats the data according to the data type
+	 *
+	 * @param string $root_element Name of the root element
+	 * @param string $type Return type (atom, rss, xml, json)
+	 * @param array $data JSON style array
+	 *
+	 * @return (string|object) XML data or JSON data
+	 */
+	function api_format_data($root_element, $type, $data){
 
 		$a = get_app();
 
@@ -621,14 +836,7 @@
 			case "atom":
 			case "rss":
 			case "xml":
-				$data = array_xmlify($data);
-				$tpl = get_markup_template("api_".$templatename."_".$type.".tpl");
-				if(! $tpl) {
-					header ("Content-Type: text/xml");
-					echo ''."\n".'not implemented';
-					killme();
-				}
-				$ret = replace_macros($tpl, $data);
+				$ret = api_create_xml($data, $root_element);
 				break;
 			case "json":
 				$ret = $data;
@@ -647,8 +855,11 @@
 	 * returns a 401 status code and an error message if not.
 	 * http://developer.twitter.com/doc/get/account/verify_credentials
 	 */
-	function api_account_verify_credentials(&$a, $type){
-		if (api_user()===false) return false;
+	function api_account_verify_credentials($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		unset($_REQUEST["user_id"]);
 		unset($_GET["user_id"]);
@@ -665,7 +876,7 @@
 
 		// - Adding last status
 		if (!$skip_status) {
-			$user_info["status"] = api_status_show($a,"raw");
+			$user_info["status"] = api_status_show("raw");
 			if (!count($user_info["status"]))
 				unset($user_info["status"]);
 			else
@@ -676,7 +887,7 @@
 		unset($user_info["uid"]);
 		unset($user_info["self"]);
 
-		return api_apply_template("user", $type, array('$user' => $user_info));
+		return api_format_data("user", $type, array('user' => $user_info));
 
 	}
 	api_register_func('api/account/verify_credentials','api_account_verify_credentials', true);
@@ -696,10 +907,13 @@
 	}
 
 /*Waitman Gobble Mod*/
-	function api_statuses_mediap(&$a, $type) {
+	function api_statuses_mediap($type) {
+
+		$a = get_app();
+
 		if (api_user()===false) {
 			logger('api_statuses_update: no user');
-			return false;
+			throw new ForbiddenException();
 		}
 		$user_info = api_get_user($a);
 
@@ -711,8 +925,6 @@
 
 		if((strpos($txt,'<') !== false) || (strpos($txt,'>') !== false)) {
 
-			require_once('library/HTMLPurifier.auto.php');
-
 			$txt = html2bb_video($txt);
 			$config = HTMLPurifier_Config::createDefault();
 			$config->set('Cache.DefinitionImpl', null);
@@ -731,16 +943,19 @@
 		item_post($a);
 
 		// this should output the last post (the one we just posted).
-		return api_status_show($a,$type);
+		return api_status_show($type);
 	}
-	api_register_func('api/statuses/mediap','api_statuses_mediap', true);
+	api_register_func('api/statuses/mediap','api_statuses_mediap', true, API_METHOD_POST);
 /*Waitman Gobble Mod*/
 
 
-	function api_statuses_update(&$a, $type) {
+	function api_statuses_update($type) {
+
+		$a = get_app();
+
 		if (api_user()===false) {
 			logger('api_statuses_update: no user');
-			return false;
+			throw new ForbiddenException();
 		}
 
 		$user_info = api_get_user($a);
@@ -752,9 +967,6 @@
 		if(requestdata('htmlstatus')) {
 			$txt = requestdata('htmlstatus');
 			if((strpos($txt,'<') !== false) || (strpos($txt,'>') !== false)) {
-
-				require_once('library/HTMLPurifier.auto.php');
-
 				$txt = html2bb_video($txt);
 
 				$config = HTMLPurifier_Config::createDefault();
@@ -805,7 +1017,8 @@
 
 				if ($posts_day > $throttle_day) {
 					logger('Daily posting limit reached for user '.api_user(), LOGGER_DEBUG);
-					die(api_error($a, $type, sprintf(t("Daily posting limit of %d posts reached. The post was rejected."), $throttle_day)));
+					#die(api_error($type, sprintf(t("Daily posting limit of %d posts reached. The post was rejected."), $throttle_day)));
+					throw new TooManyRequestsException(sprintf(t("Daily posting limit of %d posts reached. The post was rejected."), $throttle_day));
 				}
 			}
 
@@ -824,7 +1037,9 @@
 
 				if ($posts_week > $throttle_week) {
 					logger('Weekly posting limit reached for user '.api_user(), LOGGER_DEBUG);
-					die(api_error($a, $type, sprintf(t("Weekly posting limit of %d posts reached. The post was rejected."), $throttle_week)));
+					#die(api_error($type, sprintf(t("Weekly posting limit of %d posts reached. The post was rejected."), $throttle_week)));
+					throw new TooManyRequestsException(sprintf(t("Weekly posting limit of %d posts reached. The post was rejected."), $throttle_week));
+
 				}
 			}
 
@@ -843,7 +1058,8 @@
 
 				if ($posts_month > $throttle_month) {
 					logger('Monthly posting limit reached for user '.api_user(), LOGGER_DEBUG);
-					die(api_error($a, $type, sprintf(t("Monthly posting limit of %d posts reached. The post was rejected."), $throttle_month)));
+					#die(api_error($type, sprintf(t("Monthly posting limit of %d posts reached. The post was rejected."), $throttle_month)));
+					throw new TooManyRequestsException(sprintf(t("Monthly posting limit of %d posts reached. The post was rejected."), $throttle_month));
 				}
 			}
 
@@ -865,8 +1081,8 @@
 			if ($r) {
 				$phototypes = Photo::supportedTypes();
 				$ext = $phototypes[$r[0]['type']];
-				$_REQUEST['body'] .= "\n\n".'[url='.$a->get_baseurl().'/photos/'.$r[0]['nickname'].'/image/'.$r[0]['resource-id'].']';
-				$_REQUEST['body'] .= '[img]'.$a->get_baseurl()."/photo/".$r[0]['resource-id']."-".$r[0]['scale'].".".$ext."[/img][/url]";
+				$_REQUEST['body'] .= "\n\n".'[url='.App::get_baseurl().'/photos/'.$r[0]['nickname'].'/image/'.$r[0]['resource-id'].']';
+				$_REQUEST['body'] .= '[img]'.App::get_baseurl()."/photo/".$r[0]['resource-id']."-".$r[0]['scale'].".".$ext."[/img][/url]";
 			}
 		}
 
@@ -882,29 +1098,32 @@
 		item_post($a);
 
 		// this should output the last post (the one we just posted).
-		return api_status_show($a,$type);
+		return api_status_show($type);
 	}
-	api_register_func('api/statuses/update','api_statuses_update', true);
-	api_register_func('api/statuses/update_with_media','api_statuses_update', true);
+	api_register_func('api/statuses/update','api_statuses_update', true, API_METHOD_POST);
+	api_register_func('api/statuses/update_with_media','api_statuses_update', true, API_METHOD_POST);
 
 
-	function api_media_upload(&$a, $type) {
+	function api_media_upload($type) {
+
+		$a = get_app();
+
 		if (api_user()===false) {
 			logger('no user');
-			return false;
+			throw new ForbiddenException();
 		}
 
 		$user_info = api_get_user($a);
 
 		if(!x($_FILES,'media')) {
 			// Output error
-			return false;
+			throw new BadRequestException("No media.");
 		}
 
 		$media = wall_upload_post($a, false);
 		if(!$media) {
 			// Output error
-			return false;
+			throw new InternalServerErrorException();
 		}
 
 		$returndata = array();
@@ -919,10 +1138,12 @@
 
 		return array("media" => $returndata);
 	}
+	api_register_func('api/media/upload','api_media_upload', true, API_METHOD_POST);
 
-	api_register_func('api/media/upload','api_media_upload', true);
+	function api_status_show($type){
+
+		$a = get_app();
 
-	function api_status_show(&$a, $type){
 		$user_info = api_get_user($a);
 
 		logger('api_status_show: user_info: '.print_r($user_info, true), LOGGER_DEBUG);
@@ -933,13 +1154,12 @@
 			$privacy_sql = "";
 
 		// get last public wall message
-		$lastwall = q("SELECT `item`.*, `i`.`contact-id` as `reply_uid`, `i`.`author-link` AS `item-author`
-				FROM `item`, `item` as `i`
+		$lastwall = q("SELECT `item`.*
+				FROM `item`
 				WHERE `item`.`contact-id` = %d AND `item`.`uid` = %d
 					AND ((`item`.`author-link` IN ('%s', '%s')) OR (`item`.`owner-link` IN ('%s', '%s')))
-					AND `i`.`id` = `item`.`parent`
-					AND `item`.`type`!='activity' $privacy_sql
-				ORDER BY `item`.`created` DESC
+					AND `item`.`type` != 'activity' $privacy_sql
+				ORDER BY `item`.`id` DESC
 				LIMIT 1",
 				intval($user_info['cid']),
 				intval(api_user()),
@@ -952,40 +1172,15 @@
 		if (count($lastwall)>0){
 			$lastwall = $lastwall[0];
 
-			$in_reply_to_status_id = NULL;
-			$in_reply_to_user_id = NULL;
-			$in_reply_to_status_id_str = NULL;
-			$in_reply_to_user_id_str = NULL;
-			$in_reply_to_screen_name = NULL;
-			if (intval($lastwall['parent']) != intval($lastwall['id'])) {
-				$in_reply_to_status_id= intval($lastwall['parent']);
-				$in_reply_to_status_id_str = (string) intval($lastwall['parent']);
-
-				$r = q("SELECT * FROM `unique_contacts` WHERE `url` = '%s'", dbesc(normalise_link($lastwall['item-author'])));
-				if ($r) {
-					if ($r[0]['nick'] == "")
-						$r[0]['nick'] = api_get_nick($r[0]["url"]);
-
-					$in_reply_to_screen_name = (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']);
-					$in_reply_to_user_id = intval($r[0]['id']);
-					$in_reply_to_user_id_str = (string) intval($r[0]['id']);
-				}
-			}
-
-			// There seems to be situation, where both fields are identical:
-			// https://github.com/friendica/friendica/issues/1010
-			// This is a bugfix for that.
-			if (intval($in_reply_to_status_id) == intval($lastwall['id'])) {
-				logger('api_status_show: this message should never appear: id: '.$lastwall['id'].' similar to reply-to: '.$in_reply_to_status_id, LOGGER_DEBUG);
-				$in_reply_to_status_id = NULL;
-				$in_reply_to_user_id = NULL;
-				$in_reply_to_status_id_str = NULL;
-				$in_reply_to_user_id_str = NULL;
-				$in_reply_to_screen_name = NULL;
-			}
+			$in_reply_to = api_in_reply_to($lastwall);
 
 			$converted = api_convert_item($lastwall);
 
+			if ($type == "xml")
+				$geo = "georss:point";
+			else
+				$geo = "geo";
+
 			$status_info = array(
 				'created_at' => api_date($lastwall['created']),
 				'id' => intval($lastwall['id']),
@@ -993,13 +1188,13 @@
 				'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,
+				'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,
+				$geo => NULL,
 				'coordinates' => "",
 				'place' => "",
 				'contributors' => "",
@@ -1035,7 +1230,7 @@
 		if ($type == "raw")
 			return($status_info);
 
-		return  api_apply_template("status", $type, array('$status' => $status_info));
+		return  api_format_data("statuses", $type, array('status' => $status_info));
 
 	}
 
@@ -1048,17 +1243,19 @@
 	 * The author's most recent status will be returned inline.
 	 * http://developer.twitter.com/doc/get/users/show
 	 */
-	function api_users_show(&$a, $type){
-		$user_info = api_get_user($a);
+	function api_users_show($type){
 
+		$a = get_app();
+
+		$user_info = api_get_user($a);
 		$lastwall = q("SELECT `item`.*
-				FROM `item`, `contact`
+				FROM `item`
+				INNER JOIN `contact` ON `contact`.`id`=`item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
 				WHERE `item`.`uid` = %d AND `verb` = '%s' AND `item`.`contact-id` = %d
 					AND ((`item`.`author-link` IN ('%s', '%s')) OR (`item`.`owner-link` IN ('%s', '%s')))
-					AND `contact`.`id`=`item`.`contact-id`
 					AND `type`!='activity'
 					AND `item`.`allow_cid`='' AND `item`.`allow_gid`='' AND `item`.`deny_cid`='' AND `item`.`deny_gid`=''
-				ORDER BY `created` DESC
+				ORDER BY `id` DESC
 				LIMIT 1",
 				intval(api_user()),
 				dbesc(ACTIVITY_POST),
@@ -1068,48 +1265,32 @@
 				dbesc($user_info['url']),
 				dbesc(normalise_link($user_info['url']))
 		);
+
 		if (count($lastwall)>0){
 			$lastwall = $lastwall[0];
 
-			$in_reply_to_status_id = NULL;
-			$in_reply_to_user_id = NULL;
-			$in_reply_to_status_id_str = NULL;
-			$in_reply_to_user_id_str = NULL;
-			$in_reply_to_screen_name = NULL;
-			if ($lastwall['parent']!=$lastwall['id']) {
-				$reply = q("SELECT `item`.`id`, `item`.`contact-id` as `reply_uid`, `contact`.`nick` as `reply_author`, `item`.`author-link` AS `item-author`
-						FROM `item`,`contact` WHERE `contact`.`id`=`item`.`contact-id` AND `item`.`id` = %d", intval($lastwall['parent']));
-				if (count($reply)>0) {
-					$in_reply_to_status_id = intval($lastwall['parent']);
-					$in_reply_to_status_id_str = (string) intval($lastwall['parent']);
-
-					$r = q("SELECT * FROM `unique_contacts` WHERE `url` = '%s'", dbesc(normalise_link($reply[0]['item-author'])));
-					if ($r) {
-						if ($r[0]['nick'] == "")
-							$r[0]['nick'] = api_get_nick($r[0]["url"]);
-
-						$in_reply_to_screen_name = (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']);
-						$in_reply_to_user_id = intval($r[0]['id']);
-						$in_reply_to_user_id_str = (string) intval($r[0]['id']);
-					}
-				}
-			}
+			$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'] = array(
 				'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,
+				'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,
+				'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'],
@@ -1132,36 +1313,43 @@
 		unset($user_info["uid"]);
 		unset($user_info["self"]);
 
-		return  api_apply_template("user", $type, array('$user' => $user_info));
+		return  api_format_data("user", $type, array('user' => $user_info));
 
 	}
 	api_register_func('api/users/show','api_users_show');
 
 
-	function api_users_search(&$a, $type) {
+	function api_users_search($type) {
+
+		$a = get_app();
+
 		$page = (x($_REQUEST,'page')?$_REQUEST['page']-1:0);
 
 		$userlist = array();
 
 		if (isset($_GET["q"])) {
-			$r = q("SELECT id FROM `unique_contacts` WHERE `name`='%s'", dbesc($_GET["q"]));
-			if (!count($r))
-				$r = q("SELECT `id` FROM `unique_contacts` WHERE `nick`='%s'", dbesc($_GET["q"]));
+			$r = q("SELECT id FROM `contact` WHERE `uid` = 0 AND `name` = '%s'", dbesc($_GET["q"]));
+			if (!dbm::is_result($r))
+				$r = q("SELECT `id` FROM `contact` WHERE `uid` = 0 AND `nick` = '%s'", dbesc($_GET["q"]));
 
-			if (count($r)) {
+			if (dbm::is_result($r)) {
+				$k = 0;
 				foreach ($r AS $user) {
-					$user_info = api_get_user($a, $user["id"]);
-					//echo print_r($user_info, true)."\n";
-					$userdata = api_apply_template("user", $type, array('user' => $user_info));
-					$userlist[] = $userdata["user"];
+					$user_info = api_get_user($a, $user["id"], "json");
+
+					if ($type == "xml")
+						$userlist[$k++.":user"] = $user_info;
+					else
+						$userlist[] = $user_info;
 				}
 				$userlist = array("users" => $userlist);
-			} else
-				die(api_error($a, $type, t("User not found.")));
-		} else
-			die(api_error($a, $type, t("User not found.")));
-
-		return ($userlist);
+			} else {
+				throw new BadRequestException("User not found.");
+			}
+		} else {
+			throw new BadRequestException("User not found.");
+		}
+		return api_format_data("users", $type, $userlist);
 	}
 
 	api_register_func('api/users/search','api_users_search');
@@ -1173,8 +1361,11 @@
 	 * TODO: Optional parameters
 	 * TODO: Add reply info
 	 */
-	function api_statuses_home_timeline(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_home_timeline($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		unset($_REQUEST["user_id"]);
 		unset($_GET["user_id"]);
@@ -1185,7 +1376,6 @@
 		$user_info = api_get_user($a);
 		// get last newtork messages
 
-
 		// params
 		$count = (x($_REQUEST,'count')?$_REQUEST['count']:20);
 		$page = (x($_REQUEST,'page')?$_REQUEST['page']-1:0);
@@ -1206,15 +1396,15 @@
 		if ($conversation_id > 0)
 			$sql_extra .= ' AND `item`.`parent` = '.intval($conversation_id);
 
-		$r = q("SELECT STRAIGHT_JOIN `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
+		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item`, `contact`
+			`contact`.`id` AS `cid`
+			FROM `item`
+			STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
 			WHERE `item`.`uid` = %d AND `verb` = '%s'
-			AND `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `contact`.`id` = `item`.`contact-id`
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
 			$sql_extra
 			AND `item`.`id`>%d
 			ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
@@ -1224,7 +1414,7 @@
 			intval($start),	intval($count)
 		);
 
-		$ret = api_format_items($r,$user_info);
+		$ret = api_format_items($r,$user_info, false, $type);
 
 		// Set all posts from the query above to seen
 		$idarray = array();
@@ -1233,31 +1423,31 @@
 
 		$idlist = implode(",", $idarray);
 
-		if ($idlist != "")
-			$r = q("UPDATE `item` SET `unseen` = 0 WHERE `unseen` AND `id` IN (%s)", $idlist);
+		if ($idlist != "") {
+			$unseen = q("SELECT `id` FROM `item` WHERE `unseen` AND `id` IN (%s)", $idlist);
 
+			if ($unseen)
+				$r = q("UPDATE `item` SET `unseen` = 0 WHERE `unseen` AND `id` IN (%s)", $idlist);
+		}
 
-		$data = array('$statuses' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 				break;
-			case "as":
-				$as = api_format_as($a, $ret, $user_info);
-				$as['title'] = $a->config['sitename']." Home Timeline";
-				$as['link']['url'] = $a->get_baseurl()."/".$user_info["screen_name"]."/all";
-				return($as);
-				break;
 		}
 
-		return  api_apply_template("timeline", $type, $data);
+		return  api_format_data("statuses", $type, $data);
 	}
 	api_register_func('api/statuses/home_timeline','api_statuses_home_timeline', true);
 	api_register_func('api/statuses/friends_timeline','api_statuses_home_timeline', true);
 
-	function api_statuses_public_timeline(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_public_timeline($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 		// get last newtork messages
@@ -1285,15 +1475,17 @@
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`self`, `contact`.`writable`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`,
+			`contact`.`id` AS `cid`,
 			`user`.`nickname`, `user`.`hidewall`
-			FROM `item` STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id`
+			FROM `item`
+			STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
 			STRAIGHT_JOIN `user` ON `user`.`uid` = `item`.`uid`
-			WHERE `verb` = '%s' AND `item`.`visible` = 1 AND `item`.`deleted` = 0 and `item`.`moderated` = 0
+				AND NOT `user`.`hidewall`
+			WHERE `verb` = '%s' AND `item`.`visible` AND NOT `item`.`deleted` AND NOT `item`.`moderated`
 			AND `item`.`allow_cid` = ''  AND `item`.`allow_gid` = ''
 			AND `item`.`deny_cid`  = '' AND `item`.`deny_gid`  = ''
-			AND `item`.`private` = 0 AND `item`.`wall` = 1 AND `user`.`hidewall` = 0
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			AND NOT `item`.`private` AND `item`.`wall`
 			$sql_extra
 			AND `item`.`id`>%d
 			ORDER BY `item`.`id` DESC LIMIT %d, %d ",
@@ -1302,32 +1494,29 @@
 			intval($start),
 			intval($count));
 
-		$ret = api_format_items($r,$user_info);
+		$ret = api_format_items($r,$user_info, false, $type);
 
 
-		$data = array('$statuses' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 				break;
-			case "as":
-				$as = api_format_as($a, $ret, $user_info);
-				$as['title'] = $a->config['sitename']." Public Timeline";
-				$as['link']['url'] = $a->get_baseurl()."/";
-				return($as);
-				break;
 		}
 
-		return  api_apply_template("timeline", $type, $data);
+		return  api_format_data("statuses", $type, $data);
 	}
 	api_register_func('api/statuses/public_timeline','api_statuses_public_timeline', true);
 
 	/**
 	 *
 	 */
-	function api_statuses_show(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_show($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 
@@ -1347,40 +1536,37 @@
 
 		$sql_extra = '';
 		if ($conversation)
-			$sql_extra .= " AND `item`.`parent` = %d ORDER BY `received` ASC ";
+			$sql_extra .= " AND `item`.`parent` = %d ORDER BY `id` ASC ";
 		else
 			$sql_extra .= " AND `item`.`id` = %d";
 
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item`, `contact`
-			WHERE `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `contact`.`id` = `item`.`contact-id` AND `item`.`uid` = %d AND `item`.`verb` = '%s'
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			`contact`.`id` AS `cid`
+			FROM `item`
+			INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
+			WHERE `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
+			AND `item`.`uid` = %d AND `item`.`verb` = '%s'
 			$sql_extra",
 			intval(api_user()),
 			dbesc(ACTIVITY_POST),
 			intval($id)
 		);
 
-		if (!$r)
-			die(api_error($a, $type, t("There is no status with this id.")));
+		if (!$r) {
+			throw new BadRequestException("There is no status with this id.");
+		}
 
-		$ret = api_format_items($r,$user_info);
+		$ret = api_format_items($r,$user_info, false, $type);
 
 		if ($conversation) {
-			$data = array('$statuses' => $ret);
-			return api_apply_template("timeline", $type, $data);
+			$data = array('status' => $ret);
+			return api_format_data("statuses", $type, $data);
 		} else {
-			$data = array('$status' => $ret[0]);
-			/*switch($type){
-				case "atom":
-				case "rss":
-					$data = api_rss_extra($a, $data, $user_info);
-			}*/
-			return  api_apply_template("status", $type, $data);
+			$data = array('status' => $ret[0]);
+			return  api_format_data("status", $type, $data);
 		}
 	}
 	api_register_func('api/statuses/show','api_statuses_show', true);
@@ -1389,8 +1575,11 @@
 	/**
 	 *
 	 */
-	function api_conversation_show(&$a, $type){
-		if (api_user()===false) return false;
+	function api_conversation_show($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 
@@ -1422,15 +1611,21 @@
 		if ($max_id > 0)
 			$sql_extra = ' AND `item`.`id` <= '.intval($max_id);
 
+		// Not sure why this query was so complicated. We should keep it here for a while,
+		// just to make sure that we really don't need it.
+		//	FROM `item` INNER JOIN (SELECT `uri`,`parent` FROM `item` WHERE `id` = %d) AS `temp1`
+		//	ON (`item`.`thr-parent` = `temp1`.`uri` AND `item`.`parent` = `temp1`.`parent`)
+
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item` INNER JOIN (SELECT `uri`,`parent` FROM `item` WHERE `id` = %d) AS `temp1`
-			ON (`item`.`thr-parent` = `temp1`.`uri` AND `item`.`parent` = `temp1`.`parent`), `contact`
-			WHERE `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `item`.`uid` = %d AND `item`.`verb` = '%s' AND `contact`.`id` = `item`.`contact-id`
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			`contact`.`id` AS `cid`
+			FROM `item`
+			STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
+			WHERE `item`.`parent` = %d AND `item`.`visible`
+			AND NOT `item`.`moderated` AND NOT `item`.`deleted`
+			AND `item`.`uid` = %d AND `item`.`verb` = '%s'
 			AND `item`.`id`>%d $sql_extra
 			ORDER BY `item`.`id` DESC LIMIT %d ,%d",
 			intval($id), intval(api_user()),
@@ -1440,23 +1635,26 @@
 		);
 
 		if (!$r)
-			die(api_error($a, $type, t("There is no conversation with this id.")));
+			throw new BadRequestException("There is no conversation with this id.");
 
-		$ret = api_format_items($r,$user_info);
+		$ret = api_format_items($r,$user_info, false, $type);
 
-		$data = array('$statuses' => $ret);
-		return api_apply_template("timeline", $type, $data);
+		$data = array('status' => $ret);
+		return api_format_data("statuses", $type, $data);
 	}
 	api_register_func('api/conversation/show','api_conversation_show', true);
+	api_register_func('api/statusnet/conversation','api_conversation_show', true);
 
 
 	/**
 	 *
 	 */
-	function api_statuses_repeat(&$a, $type){
+	function api_statuses_repeat($type){
 		global $called_api;
 
-		if (api_user()===false) return false;
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 
@@ -1475,11 +1673,13 @@
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`, `contact`.`nick` as `reply_author`,
 			`contact`.`name`, `contact`.`photo` as `reply_photo`, `contact`.`url` as `reply_url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item`, `contact`
-			WHERE `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `contact`.`id` = `item`.`contact-id`
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			`contact`.`id` AS `cid`
+			FROM `item`
+			INNER JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
+			WHERE `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
+			AND NOT `item`.`private` AND `item`.`allow_cid` = '' AND `item`.`allow`.`gid` = ''
+			AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = ''
 			$sql_extra
 			AND `item`.`id`=%d",
 			intval($id)
@@ -1508,19 +1708,23 @@
 				$_REQUEST["source"] = api_source();
 
 			item_post($a);
-		}
+		} else
+			throw new ForbiddenException();
 
 		// this should output the last post (the one we just posted).
 		$called_api = null;
-		return(api_status_show($a,$type));
+		return(api_status_show($type));
 	}
-	api_register_func('api/statuses/retweet','api_statuses_repeat', true);
+	api_register_func('api/statuses/retweet','api_statuses_repeat', true, API_METHOD_POST);
 
 	/**
 	 *
 	 */
-	function api_statuses_destroy(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_destroy($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 
@@ -1536,21 +1740,24 @@
 
 		logger('API: api_statuses_destroy: '.$id);
 
-		$ret = api_statuses_show($a, $type);
+		$ret = api_statuses_show($type);
 
 		drop_item($id, false);
 
 		return($ret);
 	}
-	api_register_func('api/statuses/destroy','api_statuses_destroy', true);
+	api_register_func('api/statuses/destroy','api_statuses_destroy', true, API_METHOD_DELETE);
 
 	/**
 	 *
 	 * http://developer.twitter.com/doc/get/statuses/mentions
 	 *
 	 */
-	function api_statuses_mentions(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_mentions($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		unset($_REQUEST["user_id"]);
 		unset($_GET["user_id"]);
@@ -1573,7 +1780,7 @@
 		$start = $page*$count;
 
 		// Ugly code - should be changed
-		$myurl = $a->get_baseurl() . '/profile/'. $a->user['nickname'];
+		$myurl = App::get_baseurl() . '/profile/'. $a->user['nickname'];
 		$myurl = substr($myurl,strpos($myurl,'://')+3);
 		//$myurl = str_replace(array('www.','.'),array('','\\.'),$myurl);
 		$myurl = str_replace('www.','',$myurl);
@@ -1585,14 +1792,14 @@
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item`, `contact`
+			`contact`.`id` AS `cid`
+			FROM `item` FORCE INDEX (`uid_id`)
+			STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
 			WHERE `item`.`uid` = %d AND `verb` = '%s'
 			AND NOT (`item`.`author-link` IN ('https://%s', 'http://%s'))
-			AND `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `contact`.`id` = `item`.`contact-id`
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
-			AND `item`.`parent` IN (SELECT `iid` from thread where uid = %d AND `mention` AND !`ignored`)
+			AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
+			AND `item`.`parent` IN (SELECT `iid` FROM `thread` WHERE `uid` = %d AND `mention` AND !`ignored`)
 			$sql_extra
 			AND `item`.`id`>%d
 			ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
@@ -1605,31 +1812,28 @@
 			intval($start),	intval($count)
 		);
 
-		$ret = api_format_items($r,$user_info);
+		$ret = api_format_items($r,$user_info, false, $type);
 
 
-		$data = array('$statuses' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 				break;
-			case "as":
-				$as = api_format_as($a, $ret, $user_info);
-				$as["title"] = $a->config['sitename']." Mentions";
-				$as['link']['url'] = $a->get_baseurl()."/";
-				return($as);
-				break;
 		}
 
-		return  api_apply_template("timeline", $type, $data);
+		return  api_format_data("statuses", $type, $data);
 	}
 	api_register_func('api/statuses/mentions','api_statuses_mentions', true);
 	api_register_func('api/statuses/replies','api_statuses_mentions', true);
 
 
-	function api_statuses_user_timeline(&$a, $type){
-		if (api_user()===false) return false;
+	function api_statuses_user_timeline($type){
+
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$user_info = api_get_user($a);
 		// get last network messages
@@ -1662,13 +1866,13 @@
 		$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 			`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 			`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-			`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
-			FROM `item`, `contact`
+			`contact`.`id` AS `cid`
+			FROM `item` FORCE INDEX (`uid_contactid_id`)
+			STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`contact-id` AND `contact`.`uid` = `item`.`uid`
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
 			WHERE `item`.`uid` = %d AND `verb` = '%s'
 			AND `item`.`contact-id` = %d
-			AND `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
-			AND `contact`.`id` = `item`.`contact-id`
-			AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+			AND `item`.`visible` AND NOT `item`.`moderated` AND NOT `item`.`deleted`
 			$sql_extra
 			AND `item`.`id`>%d
 			ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
@@ -1679,18 +1883,17 @@
 			intval($start),	intval($count)
 		);
 
-		$ret = api_format_items($r,$user_info, true);
+		$ret = api_format_items($r,$user_info, true, $type);
 
-		$data = array('$statuses' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 		}
 
-		return  api_apply_template("timeline", $type, $data);
+		return  api_format_data("statuses", $type, $data);
 	}
-
 	api_register_func('api/statuses/user_timeline','api_statuses_user_timeline', true);
 
 
@@ -1700,15 +1903,18 @@
 	 *
 	 * api v1 : https://web.archive.org/web/20131019055350/https://dev.twitter.com/docs/api/1/post/favorites/create/%3Aid
 	 */
-	function api_favorites_create_destroy(&$a, $type){
-		if (api_user()===false) return false;
+	function api_favorites_create_destroy($type){
 
-		# for versioned api.
-		# TODO: we need a better global soluton
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
+
+		// for versioned api.
+		/// @TODO We need a better global soluton
 		$action_argv_id=2;
 		if ($a->argv[1]=="1.1") $action_argv_id=3;
 
-		if ($a->argc<=$action_argv_id) die(api_error($a, $type, t("Invalid request.")));
+		if ($a->argc<=$action_argv_id) throw new BadRequestException("Invalid request.");
 		$action = str_replace(".".$type,"",$a->argv[$action_argv_id]);
 		if ($a->argc==$action_argv_id+2) {
 			$itemid = intval($a->argv[$action_argv_id+1]);
@@ -1719,7 +1925,8 @@
 		$item = q("SELECT * FROM item WHERE id=%d AND uid=%d",
 				$itemid, api_user());
 
-		if ($item===false || count($item)==0) die(api_error($a, $type, t("Invalid item.")));
+		if ($item===false || count($item)==0)
+			throw new BadRequestException("Invalid item.");
 
 		switch($action){
 			case "create":
@@ -1729,7 +1936,7 @@
 				$item[0]['starred']=0;
 				break;
 			default:
-				die(api_error($a, $type, t("Invalid action. ".$action)));
+				throw new BadRequestException("Invalid action ".$action);
 		}
 		$r = q("UPDATE item SET starred=%d WHERE id=%d AND uid=%d",
 				$item[0]['starred'], $itemid, api_user());
@@ -1737,30 +1944,32 @@
 		q("UPDATE thread SET starred=%d WHERE iid=%d AND uid=%d",
 			$item[0]['starred'], $itemid, api_user());
 
-		if ($r===false) die(api_error($a, $type, t("DB error")));
+		if ($r===false)
+			throw InternalServerErrorException("DB error");
 
 
 		$user_info = api_get_user($a);
-		$rets = api_format_items($item,$user_info);
+		$rets = api_format_items($item, $user_info, false, $type);
 		$ret = $rets[0];
 
-		$data = array('$status' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 		}
 
-		return api_apply_template("status", $type, $data);
+		return api_format_data("status", $type, $data);
 	}
+	api_register_func('api/favorites/create', 'api_favorites_create_destroy', true, API_METHOD_POST);
+	api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true, API_METHOD_DELETE);
 
-	api_register_func('api/favorites/create', 'api_favorites_create_destroy', true);
-	api_register_func('api/favorites/destroy', 'api_favorites_create_destroy', true);
-
-	function api_favorites(&$a, $type){
+	function api_favorites($type){
 		global $called_api;
 
-		if (api_user()===false) return false;
+		$a = get_app();
+
+		if (api_user()===false) throw new ForbiddenException();
 
 		$called_api= array();
 
@@ -1790,13 +1999,13 @@
 			$r = q("SELECT `item`.*, `item`.`id` AS `item_id`, `item`.`network` AS `item_network`,
 				`contact`.`name`, `contact`.`photo`, `contact`.`url`, `contact`.`rel`,
 				`contact`.`network`, `contact`.`thumb`, `contact`.`dfrn-id`, `contact`.`self`,
-				`contact`.`id` AS `cid`, `contact`.`uid` AS `contact-uid`
+				`contact`.`id` AS `cid`
 				FROM `item`, `contact`
 				WHERE `item`.`uid` = %d
 				AND `item`.`visible` = 1 and `item`.`moderated` = 0 AND `item`.`deleted` = 0
 				AND `item`.`starred` = 1
 				AND `contact`.`id` = `item`.`contact-id`
-				AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
+				AND (NOT `contact`.`blocked` OR `contact`.`pending`)
 				$sql_extra
 				AND `item`.`id`>%d
 				ORDER BY `item`.`id` DESC LIMIT %d ,%d ",
@@ -1805,90 +2014,21 @@
 				intval($start),	intval($count)
 			);
 
-			$ret = api_format_items($r,$user_info);
+			$ret = api_format_items($r,$user_info, false, $type);
 
 		}
 
-		$data = array('$statuses' => $ret);
+		$data = array('status' => $ret);
 		switch($type){
 			case "atom":
 			case "rss":
 				$data = api_rss_extra($a, $data, $user_info);
 		}
 
-		return  api_apply_template("timeline", $type, $data);
+		return  api_format_data("statuses", $type, $data);
 	}
-
 	api_register_func('api/favorites','api_favorites', true);
 
-
-
-
-	function api_format_as($a, $ret, $user_info) {
-
-		$as = array();
-		$as['title'] = $a->config['sitename']." Public Timeline";
-		$items = array();
-		foreach ($ret as $item) {
-			$singleitem["actor"]["displayName"] = $item["user"]["name"];
-			$singleitem["actor"]["id"] = $item["user"]["contact_url"];
-			$avatar[0]["url"] = $item["user"]["profile_image_url"];
-			$avatar[0]["rel"] = "avatar";
-			$avatar[0]["type"] = "";
-			$avatar[0]["width"] = 96;
-			$avatar[0]["height"] = 96;
-			$avatar[1]["url"] = $item["user"]["profile_image_url"];
-			$avatar[1]["rel"] = "avatar";
-			$avatar[1]["type"] = "";
-			$avatar[1]["width"] = 48;
-			$avatar[1]["height"] = 48;
-			$avatar[2]["url"] = $item["user"]["profile_image_url"];
-			$avatar[2]["rel"] = "avatar";
-			$avatar[2]["type"] = "";
-			$avatar[2]["width"] = 24;
-			$avatar[2]["height"] = 24;
-			$singleitem["actor"]["avatarLinks"] = $avatar;
-
-			$singleitem["actor"]["image"]["url"] = $item["user"]["profile_image_url"];
-			$singleitem["actor"]["image"]["rel"] = "avatar";
-			$singleitem["actor"]["image"]["type"] = "";
-			$singleitem["actor"]["image"]["width"] = 96;
-			$singleitem["actor"]["image"]["height"] = 96;
-			$singleitem["actor"]["type"] = "person";
-			$singleitem["actor"]["url"] = $item["person"]["contact_url"];
-			$singleitem["actor"]["statusnet:profile_info"]["local_id"] = $item["user"]["id"];
-			$singleitem["actor"]["statusnet:profile_info"]["following"] = $item["user"]["following"] ? "true" : "false";
-			$singleitem["actor"]["statusnet:profile_info"]["blocking"] = "false";
-			$singleitem["actor"]["contact"]["preferredUsername"] = $item["user"]["screen_name"];
-			$singleitem["actor"]["contact"]["displayName"] = $item["user"]["name"];
-			$singleitem["actor"]["contact"]["addresses"] = "";
-
-			$singleitem["body"] = $item["text"];
-			$singleitem["object"]["displayName"] = $item["text"];
-			$singleitem["object"]["id"] = $item["url"];
-			$singleitem["object"]["type"] = "note";
-			$singleitem["object"]["url"] = $item["url"];
-			//$singleitem["context"] =;
-			$singleitem["postedTime"] = date("c", strtotime($item["published"]));
-			$singleitem["provider"]["objectType"] = "service";
-			$singleitem["provider"]["displayName"] = "Test";
-			$singleitem["provider"]["url"] = "http://test.tld";
-			$singleitem["title"] = $item["text"];
-			$singleitem["verb"] = "post";
-			$singleitem["statusnet:notice_info"]["local_id"] = $item["id"];
-			$singleitem["statusnet:notice_info"]["source"] = $item["source"];
-			$singleitem["statusnet:notice_info"]["favorite"] = "false";
-			$singleitem["statusnet:notice_info"]["repeated"] = "false";
-			//$singleitem["original"] = $item;
-			$items[] = $singleitem;
-		}
-		$as['items'] = $items;
-		$as['link']['url'] = $a->get_baseurl()."/".$user_info["screen_name"]."/all";
-		$as['link']['rel'] = "alternate";
-		$as['link']['type'] = "text/html";
-		return($as);
-	}
-
 	function api_format_messages($item, $recipient, $sender) {
 		// standard meta information
 		$ret=Array(
@@ -1901,6 +2041,9 @@
 				'recipient_screen_name' => $recipient['screen_name'],
 				'sender'                => $sender,
 				'recipient'             => $recipient,
+				'title'			=> "",
+				'friendica_seen'	=> $item['seen'],
+				'friendica_parent_uri'	=> $item['parent-uri'],
 		);
 
 		// "uid" and "self" are only needed for some internal stuff, so remove it from here
@@ -1932,7 +2075,6 @@
 	}
 
 	function api_convert_item($item) {
-
 		$body = $item['body'];
 		$attachments = api_get_attachments($body);
 
@@ -1955,12 +2097,27 @@
 
 		$statushtml = trim(bbcode($body, false, false));
 
+		$search = array("
", "
", "
", + "

", "

", "

", "

", + "

", "

", "

", "

", + "
", "
", "
", "
"); + $replace = array("
\n", "\n
", "
\n", + "\n

", "

\n", "\n

", "

\n", + "\n

", "

\n", "\n

", "

\n", + "\n
", "
\n", "\n
", "
\n"); + $statushtml = str_replace($search, $replace, $statushtml); + if ($item['title'] != "") $statushtml = "

".bbcode($item['title'])."

\n".$statushtml; $entities = api_get_entitities($statustext, $body); - return(array("text" => $statustext, "html" => $statushtml, "attachments" => $attachments, "entities" => $entities)); + return array( + "text" => $statustext, + "html" => $statushtml, + "attachments" => $attachments, + "entities" => $entities + ); } function api_get_attachments(&$body) { @@ -2144,90 +2301,216 @@ return($entities); } - function api_format_items_embeded_images($item, $text){ - $a = get_app(); + function api_format_items_embeded_images(&$item, $text){ $text = preg_replace_callback( "|data:image/([^;]+)[^=]+=*|m", - function($match) use ($a, $item) { - return $a->get_baseurl()."/display/".$item['guid']; + function($match) use ($item) { + return App::get_baseurl()."/display/".$item['guid']; }, $text); return $text; } - function api_format_items($r,$user_info, $filter_user = false) { + + /** + * @brief return name as array + * + * @param string $txt + * @return array + * name => 'name' + * 'url => 'url' + */ + function api_contactlink_to_array($txt) { + $match = array(); + $r = preg_match_all('|([^<]*)|', $txt, $match); + if ($r && count($match)==3) { + $res = array( + 'name' => $match[2], + 'url' => $match[1] + ); + } else { + $res = array( + 'name' => $text, + 'url' => "" + ); + } + return $res; + } + + + /** + * @brief return likes, dislikes and attend status for item + * + * @param array $item + * @return array + * likes => int count + * dislikes => int count + */ + function api_format_items_activities(&$item, $type = "json") { $a = get_app(); + + $activities = array( + 'like' => array(), + 'dislike' => array(), + 'attendyes' => array(), + 'attendno' => array(), + 'attendmaybe' => array() + ); + + $items = q('SELECT * FROM item + WHERE uid=%d AND `thr-parent`="%s" AND visible AND NOT deleted', + intval($item['uid']), + dbesc($item['uri'])); + + foreach ($items as $i){ + // not used as result should be structured like other user data + //builtin_activity_puller($i, $activities); + + // get user data and add it to the array of the activity + $user = api_get_user($a, $i['author-link']); + switch($i['verb']) { + case ACTIVITY_LIKE: + $activities['like'][] = $user; + break; + case ACTIVITY_DISLIKE: + $activities['dislike'][] = $user; + break; + case ACTIVITY_ATTEND: + $activities['attendyes'][] = $user; + break; + case ACTIVITY_ATTENDNO: + $activities['attendno'][] = $user; + break; + case ACTIVITY_ATTENDMAYBE: + $activities['attendmaybe'][] = $user; + break; + default: + break; + } + } + + if ($type == "xml") { + $xml_activities = array(); + foreach ($activities as $k => $v) { + // change xml element from "like" to "friendica:like" + $xml_activities["friendica:".$k] = $v; + // add user data into xml output + $k_user = 0; + foreach ($v as $user) + $xml_activities["friendica:".$k][$k_user++.":user"] = $user; + } + $activities = $xml_activities; + } + + return $activities; + + } + + + /** + * @brief return data from profiles + * + * @param array $profile array containing data from db table 'profile' + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return array + */ + function api_format_items_profiles(&$profile = null, $type = "json") { + if ($profile != null) { + $profile = array('profile_id' => $profile['id'], + 'profile_name' => $profile['profile-name'], + 'is_default' => $profile['is-default'] ? true : false, + 'hide_friends'=> $profile['hide-friends'] ? true : false, + 'profile_photo' => $profile['photo'], + 'profile_thumb' => $profile['thumb'], + 'publish' => $profile['publish'] ? true : false, + 'net_publish' => $profile['net-publish'] ? true : false, + 'description' => $profile['pdesc'], + 'date_of_birth' => $profile['dob'], + 'address' => $profile['address'], + 'city' => $profile['locality'], + 'region' => $profile['region'], + 'postal_code' => $profile['postal-code'], + 'country' => $profile['country-name'], + 'hometown' => $profile['hometown'], + 'gender' => $profile['gender'], + 'marital' => $profile['marital'], + 'marital_with' => $profile['with'], + 'marital_since' => $profile['howlong'], + 'sexual' => $profile['sexual'], + 'politic' => $profile['politic'], + 'religion' => $profile['religion'], + 'public_keywords' => $profile['pub_keywords'], + 'private_keywords' => $profile['prv_keywords'], + 'likes' => bbcode(api_clean_plain_items($profile['likes']), false, false, 2, false), + 'dislikes' => bbcode(api_clean_plain_items($profile['dislikes']), false, false, 2, false), + 'about' => bbcode(api_clean_plain_items($profile['about']), false, false, 2, false), + 'music' => bbcode(api_clean_plain_items($profile['music']), false, false, 2, false), + 'book' => bbcode(api_clean_plain_items($profile['book']), false, false, 2, false), + 'tv' => bbcode(api_clean_plain_items($profile['tv']), false, false, 2, false), + 'film' => bbcode(api_clean_plain_items($profile['film']), false, false, 2, false), + 'interest' => bbcode(api_clean_plain_items($profile['interest']), false, false, 2, false), + 'romance' => bbcode(api_clean_plain_items($profile['romance']), false, false, 2, false), + 'work' => bbcode(api_clean_plain_items($profile['work']), false, false, 2, false), + 'education' => bbcode(api_clean_plain_items($profile['education']), false, false, 2, false), + 'social_networks' => bbcode(api_clean_plain_items($profile['contact']), false, false, 2, false), + 'homepage' => $profile['homepage'], + 'users' => null); + return $profile; + } + } + + /** + * @brief format items to be returned by api + * + * @param array $r array of items + * @param array $user_info + * @param bool $filter_user filter items by $user_info + */ + function api_format_items($r,$user_info, $filter_user = false, $type = "json") { + + $a = get_app(); + $ret = Array(); foreach($r as $item) { - api_share_as_retweet($item); localize_item($item); - $status_user = api_item_get_user($a,$item); + list($status_user, $owner_user) = api_item_get_user($a,$item); // Look if the posts are matching if they should be filtered by user id if ($filter_user AND ($status_user["id"] != $user_info["id"])) continue; - if ($item['thr-parent'] != $item['uri']) { - $r = q("SELECT id FROM item WHERE uid=%d AND uri='%s' LIMIT 1", - intval(api_user()), - dbesc($item['thr-parent'])); - if ($r) - $in_reply_to_status_id = intval($r[0]['id']); - else - $in_reply_to_status_id = intval($item['parent']); - - $in_reply_to_status_id_str = (string) intval($item['parent']); - - $in_reply_to_screen_name = NULL; - $in_reply_to_user_id = NULL; - $in_reply_to_user_id_str = NULL; - - $r = q("SELECT `author-link` FROM item WHERE uid=%d AND id=%d LIMIT 1", - intval(api_user()), - intval($in_reply_to_status_id)); - if ($r) { - $r = q("SELECT * FROM `unique_contacts` WHERE `url` = '%s'", dbesc(normalise_link($r[0]['author-link']))); - - if ($r) { - if ($r[0]['nick'] == "") - $r[0]['nick'] = api_get_nick($r[0]["url"]); - - $in_reply_to_screen_name = (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']); - $in_reply_to_user_id = intval($r[0]['id']); - $in_reply_to_user_id_str = (string) intval($r[0]['id']); - } - } - } else { - $in_reply_to_screen_name = NULL; - $in_reply_to_user_id = NULL; - $in_reply_to_status_id = NULL; - $in_reply_to_user_id_str = NULL; - $in_reply_to_status_id_str = NULL; - } + $in_reply_to = api_in_reply_to($item); $converted = api_convert_item($item); + if ($type == "xml") + $geo = "georss:point"; + else + $geo = "geo"; + $status = array( '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, + '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, + '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_owner' => $owner_user, //'entities' => NULL, 'statusnet_html' => $converted["html"], 'statusnet_conversation_id' => $item['parent'], + 'friendica_activities' => api_format_items_activities($item, $type), ); if (count($converted["attachments"]) > 0) @@ -2244,15 +2527,31 @@ // Retweets are only valid for top postings // It doesn't work reliable with the link if its a feed - $IsRetweet = ($item['owner-link'] != $item['author-link']); - if ($IsRetweet) - $IsRetweet = (($item['owner-name'] != $item['author-name']) OR ($item['owner-avatar'] != $item['author-avatar'])); + //$IsRetweet = ($item['owner-link'] != $item['author-link']); + //if ($IsRetweet) + // $IsRetweet = (($item['owner-name'] != $item['author-name']) OR ($item['owner-avatar'] != $item['author-avatar'])); - if ($IsRetweet AND ($item["id"] == $item["parent"])) { - $retweeted_status = $status; - $retweeted_status["user"] = api_get_user($a,$item["author-link"]); - $status["retweeted_status"] = $retweeted_status; + if ($item["id"] == $item["parent"]) { + $retweeted_item = api_share_as_retweet($item); + if ($retweeted_item !== false) { + $retweeted_status = $status; + try { + $retweeted_status["user"] = api_get_user($a,$retweeted_item["author-link"]); + } catch( BadRequestException $e ) { + // user not found. should be found? + /// @todo check if the user should be always found + $retweeted_status["user"] = array(); + } + + $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; + } } // "uid" and "self" are only needed for some internal stuff, so remove it from here @@ -2262,57 +2561,64 @@ if ($item["coord"] != "") { $coords = explode(' ',$item["coord"]); if (count($coords) == 2) { - $status["geo"] = array('type' => 'Point', - 'coordinates' => array((float) $coords[0], - (float) $coords[1])); + if ($type == "json") + $status["geo"] = array('type' => 'Point', + 'coordinates' => array((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; } - function api_account_rate_limit_status(&$a,$type) { + function api_account_rate_limit_status($type) { - $hash = array( - 'reset_time_in_seconds' => strtotime('now + 1 hour'), - 'remaining_hits' => (string) 150, - 'hourly_limit' => (string) 150, - 'reset_time' => api_date(datetime_convert('UTC','UTC','now + 1 hour',ATOM_TIME)), - ); if ($type == "xml") - $hash['resettime_in_seconds'] = $hash['reset_time_in_seconds']; - - return api_apply_template('ratelimit', $type, array('$hash' => $hash)); + $hash = array( + 'remaining-hits' => (string) 150, + '@attributes' => array("type" => "integer"), + 'hourly-limit' => (string) 150, + '@attributes2' => array("type" => "integer"), + 'reset-time' => datetime_convert('UTC','UTC','now + 1 hour',ATOM_TIME), + '@attributes3' => array("type" => "datetime"), + 'reset_time_in_seconds' => strtotime('now + 1 hour'), + '@attributes4' => array("type" => "integer"), + ); + else + $hash = array( + 'reset_time_in_seconds' => strtotime('now + 1 hour'), + 'remaining_hits' => (string) 150, + 'hourly_limit' => (string) 150, + 'reset_time' => api_date(datetime_convert('UTC','UTC','now + 1 hour',ATOM_TIME)), + ); + return api_format_data('hash', $type, array('hash' => $hash)); } api_register_func('api/account/rate_limit_status','api_account_rate_limit_status',true); - function api_help_test(&$a,$type) { - + function api_help_test($type) { if ($type == 'xml') $ok = "true"; else $ok = "ok"; - return api_apply_template('test', $type, array("$ok" => $ok)); - + return api_format_data('ok', $type, array("ok" => $ok)); } api_register_func('api/help/test','api_help_test',false); - function api_lists(&$a,$type) { - + function api_lists($type) { $ret = array(); - return array($ret); + return api_format_data('lists', $type, array("lists_list" => $ret)); } api_register_func('api/lists','api_lists',true); - function api_lists_list(&$a,$type) { - + function api_lists_list($type) { $ret = array(); - return array($ret); + return api_format_data('lists', $type, array("lists_list" => $ret)); } api_register_func('api/lists/list','api_lists_list',true); @@ -2321,8 +2627,11 @@ * This function is deprecated by Twitter * returns: json, xml **/ - function api_statuses_f(&$a, $type, $qtype) { - if (api_user()===false) return false; + function api_statuses_f($type, $qtype) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); $user_info = api_get_user($a); if (x($_GET,'cursor') && $_GET['cursor']=='undefined'){ @@ -2345,7 +2654,7 @@ if ($user_info['self'] == 0) $sql_extra = " AND false "; - $r = q("SELECT `nurl` FROM `contact` WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `pending` = 0 $sql_extra", + $r = q("SELECT `nurl` FROM `contact` WHERE `uid` = %d AND NOT `self` AND (NOT `blocked` OR `pending`) $sql_extra", intval(api_user()) ); @@ -2360,18 +2669,18 @@ $ret[] = $user; } - return array('$users' => $ret); + return array('user' => $ret); } - function api_statuses_friends(&$a, $type){ - $data = api_statuses_f($a,$type,"friends"); + function api_statuses_friends($type){ + $data = api_statuses_f($type, "friends"); if ($data===false) return false; - return api_apply_template("friends", $type, $data); + return api_format_data("users", $type, $data); } - function api_statuses_followers(&$a, $type){ - $data = api_statuses_f($a,$type,"followers"); + function api_statuses_followers($type){ + $data = api_statuses_f($type, "followers"); if ($data===false) return false; - return api_apply_template("friends", $type, $data); + return api_format_data("users", $type, $data); } api_register_func('api/statuses/friends','api_statuses_friends',true); api_register_func('api/statuses/followers','api_statuses_followers',true); @@ -2381,18 +2690,21 @@ - function api_statusnet_config(&$a,$type) { + function api_statusnet_config($type) { + + $a = get_app(); + $name = $a->config['sitename']; $server = $a->get_hostname(); - $logo = $a->get_baseurl() . '/images/friendica-64.png'; + $logo = App::get_baseurl() . '/images/friendica-64.png'; $email = $a->config['admin_email']; $closed = (($a->config['register_policy'] == REGISTER_CLOSED) ? 'true' : 'false'); - $private = (($a->config['system']['block_public']) ? 'true' : 'false'); + $private = ((Config::get('system', 'block_public')) ? 'true' : 'false'); $textlimit = (string) (($a->config['max_import_size']) ? $a->config['max_import_size'] : 200000); if($a->config['api_import_size']) $texlimit = string($a->config['api_import_size']); - $ssl = (($a->config['system']['have_ssl']) ? 'true' : 'false'); - $sslserver = (($ssl === 'true') ? str_replace('http:','https:',$a->get_baseurl()) : ''); + $ssl = ((Config::get('system', 'have_ssl')) ? 'true' : 'false'); + $sslserver = (($ssl === 'true') ? str_replace('http:','https:',App::get_baseurl()) : ''); $config = array( 'site' => array('name' => $name,'server' => $server, 'theme' => 'default', 'path' => '', @@ -2409,32 +2721,27 @@ ), ); - return api_apply_template('config', $type, array('$config' => $config)); + return api_format_data('config', $type, array('config' => $config)); } api_register_func('api/statusnet/config','api_statusnet_config',false); - function api_statusnet_version(&$a,$type) { - + function api_statusnet_version($type) { // liar + $fake_statusnet_version = "0.9.7"; - if($type === 'xml') { - header("Content-type: application/xml"); - echo '' . "\r\n" . '0.9.7' . "\r\n"; - killme(); - } - elseif($type === 'json') { - header("Content-type: application/json"); - echo '"0.9.7"'; - killme(); - } + return api_format_data('version', $type, array('version' => $fake_statusnet_version)); } api_register_func('api/statusnet/version','api_statusnet_version',false); + /** + * @todo use api_format_data() to return data + */ + function api_ff_ids($type,$qtype) { - function api_ff_ids(&$a,$type,$qtype) { - if(! api_user()) - return false; + $a = get_app(); + + if(! api_user()) throw new ForbiddenException(); $user_info = api_get_user($a); @@ -2448,47 +2755,40 @@ $stringify_ids = (x($_REQUEST,'stringify_ids')?$_REQUEST['stringify_ids']:false); - $r = q("SELECT `unique_contact`.`id` FROM contact, `unique_contacts` WHERE contact.nurl = unique_contacts.url AND `uid` = %d AND `self` = 0 AND `blocked` = 0 AND `pending` = 0 $sql_extra", + $r = q("SELECT `pcontact`.`id` FROM `contact` + INNER JOIN `contact` AS `pcontact` ON `contact`.`nurl` = `pcontact`.`nurl` AND `pcontact`.`uid` = 0 + WHERE `contact`.`uid` = %s AND NOT `contact`.`self`", intval(api_user()) ); - if(is_array($r)) { + if (!dbm::is_result($r)) + return; - if($type === 'xml') { - header("Content-type: application/xml"); - echo '' . "\r\n" . '' . "\r\n"; - foreach($r as $rr) - echo '' . $rr['id'] . '' . "\r\n"; - echo '' . "\r\n"; - killme(); - } - elseif($type === 'json') { - $ret = array(); - header("Content-type: application/json"); - foreach($r as $rr) - if ($stringify_ids) - $ret[] = $rr['id']; - else - $ret[] = intval($rr['id']); + $ids = array(); + foreach($r as $rr) + if ($stringify_ids) + $ids[] = $rr['id']; + else + $ids[] = intval($rr['id']); - echo json_encode($ret); - killme(); - } - } + return api_format_data("ids", $type, array('id' => $ids)); } - function api_friends_ids(&$a,$type) { - api_ff_ids($a,$type,'friends'); + function api_friends_ids($type) { + return api_ff_ids($type,'friends'); } - function api_followers_ids(&$a,$type) { - api_ff_ids($a,$type,'followers'); + function api_followers_ids($type) { + return api_ff_ids($type,'followers'); } api_register_func('api/friends/ids','api_friends_ids',true); api_register_func('api/followers/ids','api_followers_ids',true); - function api_direct_messages_new(&$a, $type) { - if (api_user()===false) return false; + function api_direct_messages_new($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); if (!x($_POST, "text") OR (!x($_POST,"screen_name") AND !x($_POST,"user_id"))) return; @@ -2534,7 +2834,7 @@ $ret = array("error"=>$id); } - $data = Array('$messages'=>$ret); + $data = Array('direct_message'=>$ret); switch($type){ case "atom": @@ -2542,14 +2842,89 @@ $data = api_rss_extra($a, $data, $user_info); } - return api_apply_template("direct_messages", $type, $data); + return api_format_data("direct-messages", $type, $data); } - api_register_func('api/direct_messages/new','api_direct_messages_new',true); + api_register_func('api/direct_messages/new','api_direct_messages_new',true, API_METHOD_POST); - function api_direct_messages_box(&$a, $type, $box) { - if (api_user()===false) return false; + /** + * @brief delete a direct_message from mail table through api + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string + */ + function api_direct_messages_destroy($type){ + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + //required + $id = (x($_REQUEST,'id') ? $_REQUEST['id'] : 0); + // optional + $parenturi = (x($_REQUEST, 'friendica_parenturi') ? $_REQUEST['friendica_parenturi'] : ""); + $verbose = (x($_GET,'friendica_verbose')?strtolower($_GET['friendica_verbose']):"false"); + /// @todo optional parameter 'include_entities' from Twitter API not yet implemented + + $uid = $user_info['uid']; + // error if no id or parenturi specified (for clients posting parent-uri as well) + if ($verbose == "true") { + if ($id == 0 || $parenturi == "") { + $answer = array('result' => 'error', 'message' => 'message id or parenturi not specified'); + return api_format_data("direct_messages_delete", $type, array('$result' => $answer)); + } + } + + // BadRequestException if no id specified (for clients using Twitter API) + if ($id == 0) throw new BadRequestException('Message id not specified'); + + // add parent-uri to sql command if specified by calling app + $sql_extra = ($parenturi != "" ? " AND `parent-uri` = '" . dbesc($parenturi) . "'" : ""); + + // get data of the specified message id + $r = q("SELECT `id` FROM `mail` WHERE `uid` = %d AND `id` = %d" . $sql_extra, + intval($uid), + intval($id)); + + // error message if specified id is not in database + if (!dbm::is_result($r)) { + if ($verbose == "true") { + $answer = array('result' => 'error', 'message' => 'message id not in database'); + return api_format_data("direct_messages_delete", $type, array('$result' => $answer)); + } + /// @todo BadRequestException ok for Twitter API clients? + throw new BadRequestException('message id not in database'); + } + + // delete message + $result = q("DELETE FROM `mail` WHERE `uid` = %d AND `id` = %d" . $sql_extra, + intval($uid), + intval($id)); + + if ($verbose == "true") { + if ($result) { + // return success + $answer = array('result' => 'ok', 'message' => 'message deleted'); + return api_format_data("direct_message_delete", $type, array('$result' => $answer)); + } + else { + $answer = array('result' => 'error', 'message' => 'unknown error'); + return api_format_data("direct_messages_delete", $type, array('$result' => $answer)); + } + } + /// @todo return JSON data like Twitter API not yet implemented + + } + api_register_func('api/direct_messages/destroy', 'api_direct_messages_destroy', true, API_METHOD_DELETE); + + + function api_direct_messages_box($type, $box, $verbose) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); // params $count = (x($_GET,'count')?$_GET['count']:20); @@ -2570,7 +2945,6 @@ unset($_GET["screen_name"]); $user_info = api_get_user($a); - //$profile_url = $a->get_baseurl() . '/profile/' . $a->user['nickname']; $profile_url = $user_info["url"]; @@ -2606,7 +2980,13 @@ intval($since_id), intval($start), intval($count) ); - + if ($verbose == "true") { + // stop execution and return error message if no mails available + if($r == null) { + $answer = array('result' => 'error', 'message' => 'no mails available'); + return api_format_data("direct_messages_all", $type, array('$result' => $answer)); + } + } $ret = Array(); foreach($r as $item) { @@ -2623,28 +3003,32 @@ } - $data = array('$messages' => $ret); + $data = array('direct_message' => $ret); switch($type){ case "atom": case "rss": $data = api_rss_extra($a, $data, $user_info); } - return api_apply_template("direct_messages", $type, $data); + return api_format_data("direct-messages", $type, $data); } - function api_direct_messages_sentbox(&$a, $type){ - return api_direct_messages_box($a, $type, "sentbox"); + function api_direct_messages_sentbox($type){ + $verbose = (x($_GET,'friendica_verbose')?strtolower($_GET['friendica_verbose']):"false"); + return api_direct_messages_box($type, "sentbox", $verbose); } - function api_direct_messages_inbox(&$a, $type){ - return api_direct_messages_box($a, $type, "inbox"); + function api_direct_messages_inbox($type){ + $verbose = (x($_GET,'friendica_verbose')?strtolower($_GET['friendica_verbose']):"false"); + return api_direct_messages_box($type, "inbox", $verbose); } - function api_direct_messages_all(&$a, $type){ - return api_direct_messages_box($a, $type, "all"); + function api_direct_messages_all($type){ + $verbose = (x($_GET,'friendica_verbose')?strtolower($_GET['friendica_verbose']):"false"); + return api_direct_messages_box($type, "all", $verbose); } - function api_direct_messages_conversation(&$a, $type){ - return api_direct_messages_box($a, $type, "conversation"); + function api_direct_messages_conversation($type){ + $verbose = (x($_GET,'friendica_verbose')?strtolower($_GET['friendica_verbose']):"false"); + return api_direct_messages_box($type, "conversation", $verbose); } api_register_func('api/direct_messages/conversation','api_direct_messages_conversation',true); api_register_func('api/direct_messages/all','api_direct_messages_all',true); @@ -2653,7 +3037,7 @@ - function api_oauth_request_token(&$a, $type){ + function api_oauth_request_token($type){ try{ $oauth = new FKOAuth1(); $r = $oauth->fetch_request_token(OAuthRequest::from_request()); @@ -2663,7 +3047,7 @@ echo $r; killme(); } - function api_oauth_access_token(&$a, $type){ + function api_oauth_access_token($type){ try{ $oauth = new FKOAuth1(); $r = $oauth->fetch_access_token(OAuthRequest::from_request()); @@ -2678,37 +3062,90 @@ api_register_func('api/oauth/access_token', 'api_oauth_access_token', false); - function api_fr_photos_list(&$a,$type) { - if (api_user()===false) return false; - $r = q("select distinct `resource-id` from photo where uid = %d and album != 'Contact Photos' ", + function api_fr_photos_list($type) { + if (api_user()===false) throw new ForbiddenException(); + $r = q("select `resource-id`, max(scale) as scale, album, filename, type from photo + where uid = %d and album != 'Contact Photos' group by `resource-id`", intval(local_user()) ); - if($r) { - $ret = array(); - foreach($r as $rr) - $ret[] = $rr['resource-id']; - header("Content-type: application/json"); - echo json_encode($ret); + $typetoext = array( + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif' + ); + $data = array('photo'=>array()); + if ($r) { + foreach ($r as $rr) { + $photo = array(); + $photo['id'] = $rr['resource-id']; + $photo['album'] = $rr['album']; + $photo['filename'] = $rr['filename']; + $photo['type'] = $rr['type']; + $thumb = App::get_baseurl()."/photo/".$rr['resource-id']."-".$rr['scale'].".".$typetoext[$rr['type']]; + + if ($type == "xml") + $data['photo'][] = array("@attributes" => $photo, "1" => $thumb); + else { + $photo['thumb'] = $thumb; + $data['photo'][] = $photo; + } + } } - killme(); + return api_format_data("photos", $type, $data); } - function api_fr_photo_detail(&$a,$type) { - if (api_user()===false) return false; - if(! $_REQUEST['photo_id']) return false; - $scale = ((array_key_exists('scale',$_REQUEST)) ? intval($_REQUEST['scale']) : 0); - $r = q("select * from photo where uid = %d and `resource-id` = '%s' and scale = %d limit 1", + function api_fr_photo_detail($type) { + if (api_user()===false) throw new ForbiddenException(); + if(!x($_REQUEST,'photo_id')) throw new BadRequestException("No photo id."); + + $scale = (x($_REQUEST, 'scale') ? intval($_REQUEST['scale']) : false); + $scale_sql = ($scale === false ? "" : sprintf("and scale=%d",intval($scale))); + $data_sql = ($scale === false ? "" : "data, "); + + $r = q("select %s `resource-id`, `created`, `edited`, `title`, `desc`, `album`, `filename`, + `type`, `height`, `width`, `datasize`, `profile`, min(`scale`) as minscale, max(`scale`) as maxscale + from photo where `uid` = %d and `resource-id` = '%s' %s group by `resource-id`", + $data_sql, intval(local_user()), dbesc($_REQUEST['photo_id']), - intval($scale) + $scale_sql ); - if($r) { - header("Content-type: application/json"); - $r[0]['data'] = base64_encode($r[0]['data']); - echo json_encode($r[0]); + + $typetoext = array( + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif' + ); + + if ($r) { + $data = array('photo' => $r[0]); + $data['photo']['id'] = $data['photo']['resource-id']; + if ($scale !== false) { + $data['photo']['data'] = base64_encode($data['photo']['data']); + } else { + unset($data['photo']['datasize']); //needed only with scale param + } + if ($type == "xml") { + $data['photo']['links'] = array(); + for ($k=intval($data['photo']['minscale']); $k<=intval($data['photo']['maxscale']); $k++) + $data['photo']['links'][$k.":link"]["@attributes"] = array("type" => $data['photo']['type'], + "scale" => $k, + "href" => App::get_baseurl()."/photo/".$data['photo']['resource-id']."-".$k.".".$typetoext[$data['photo']['type']]); + } else { + $data['photo']['link'] = array(); + for ($k=intval($data['photo']['minscale']); $k<=intval($data['photo']['maxscale']); $k++) { + $data['photo']['link'][$k] = App::get_baseurl()."/photo/".$data['photo']['resource-id']."-".$k.".".$typetoext[$data['photo']['type']]; + } + } + unset($data['photo']['resource-id']); + unset($data['photo']['minscale']); + unset($data['photo']['maxscale']); + + } else { + throw new NotFoundException(); } - killme(); + return api_format_data("photo_detail", $type, $data); } api_register_func('api/friendica/photos/list', 'api_fr_photos_list', true); @@ -2727,12 +3164,12 @@ * c_url: url of remote contact to auth to * url: string, url to redirect after auth */ - function api_friendica_remoteauth(&$a) { + function api_friendica_remoteauth() { $url = ((x($_GET,'url')) ? $_GET['url'] : ''); $c_url = ((x($_GET,'c_url')) ? $_GET['c_url'] : ''); if ($url === '' || $c_url === '') - die((api_error($a, 'json', "Wrong parameters"))); + throw new BadRequestException("Wrong parameters."); $c_url = normalise_link($c_url); @@ -2743,8 +3180,8 @@ intval(api_user()) ); - if ((! count($r)) || ($r[0]['network'] !== NETWORK_DFRN)) - die((api_error($a, 'json', "Unknown contact"))); + if ((! dbm::is_result($r)) || ($r[0]['network'] !== NETWORK_DFRN)) + throw new BadRequestException("Unknown contact"); $cid = $r[0]['id']; @@ -2778,240 +3215,804 @@ } api_register_func('api/friendica/remoteauth', 'api_friendica_remoteauth', true); + /** + * @brief Return the item shared, if the item contains only the [share] tag + * + * @param array $item Sharer item + * @return array Shared item or false if not a reshare + */ + function api_share_as_retweet(&$item) { + $body = trim($item["body"]); + + if (Diaspora::is_reshare($body, false)===false) { + return false; + } + + $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body); + // Skip if there is no shared message in there + // we already checked this in diaspora::is_reshare() + // but better one more than one less... + if ($body == $attributes) + return false; -function api_share_as_retweet(&$item) { - $body = trim($item["body"]); + // build the fake reshared item + $reshared_item = $item; - // Skip if it isn't a pure repeated messages - // Does it start with a share? - if (strpos($body, "[share") > 0) - return(false); + $author = ""; + preg_match("/author='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $author = html_entity_decode($matches[1],ENT_QUOTES,'UTF-8'); - // Does it end with a share? - if (strlen($body) > (strrpos($body, "[/share]") + 8)) - return(false); + preg_match('/author="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $author = $matches[1]; - $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body); - // Skip if there is no shared message in there - if ($body == $attributes) - return(false); + $profile = ""; + preg_match("/profile='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $profile = $matches[1]; - $author = ""; - preg_match("/author='(.*?)'/ism", $attributes, $matches); - if ($matches[1] != "") - $author = html_entity_decode($matches[1],ENT_QUOTES,'UTF-8'); + preg_match('/profile="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $profile = $matches[1]; - preg_match('/author="(.*?)"/ism', $attributes, $matches); - if ($matches[1] != "") - $author = $matches[1]; + $avatar = ""; + preg_match("/avatar='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $avatar = $matches[1]; - $profile = ""; - preg_match("/profile='(.*?)'/ism", $attributes, $matches); - if ($matches[1] != "") - $profile = $matches[1]; + preg_match('/avatar="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $avatar = $matches[1]; - preg_match('/profile="(.*?)"/ism', $attributes, $matches); - if ($matches[1] != "") - $profile = $matches[1]; + $link = ""; + preg_match("/link='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $link = $matches[1]; - $avatar = ""; - preg_match("/avatar='(.*?)'/ism", $attributes, $matches); - if ($matches[1] != "") - $avatar = $matches[1]; + preg_match('/link="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $link = $matches[1]; - preg_match('/avatar="(.*?)"/ism', $attributes, $matches); - if ($matches[1] != "") - $avatar = $matches[1]; + $posted = ""; + preg_match("/posted='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $posted= $matches[1]; - $link = ""; - preg_match("/link='(.*?)'/ism", $attributes, $matches); - if ($matches[1] != "") - $link = $matches[1]; + preg_match('/posted="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $posted = $matches[1]; - preg_match('/link="(.*?)"/ism', $attributes, $matches); - if ($matches[1] != "") - $link = $matches[1]; + $shared_body = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$2",$body); - $shared_body = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$2",$body); + if (($shared_body == "") || ($profile == "") || ($author == "") || ($avatar == "") || ($posted == "")) + return false; - if (($shared_body == "") OR ($profile == "") OR ($author == "") OR ($avatar == "")) - return(false); - $item["body"] = $shared_body; - $item["author-name"] = $author; - $item["author-link"] = $profile; - $item["author-avatar"] = $avatar; - $item["plink"] = $link; - return(true); + $reshared_item["body"] = $shared_body; + $reshared_item["author-name"] = $author; + $reshared_item["author-link"] = $profile; + $reshared_item["author-avatar"] = $avatar; + $reshared_item["plink"] = $link; + $reshared_item["created"] = $posted; + $reshared_item["edited"] = $posted; -} + return $reshared_item; -function api_get_nick($profile) { -/* To-Do: - - remove trailing junk from profile url - - pump.io check has to check the website -*/ + } - $nick = ""; + function api_get_nick($profile) { + /* To-Do: + - remove trailing junk from profile url + - pump.io check has to check the website + */ - $r = q("SELECT `nick` FROM `gcontact` WHERE `nurl` = '%s'", - dbesc(normalise_link($profile))); - if ($r) - $nick = $r[0]["nick"]; + $nick = ""; - if (!$nick == "") { $r = q("SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'", dbesc(normalise_link($profile))); if ($r) $nick = $r[0]["nick"]; - } - if (!$nick == "") { - $friendica = preg_replace("=https?://(.*)/profile/(.*)=ism", "$2", $profile); - if ($friendica != $profile) - $nick = $friendica; - } + if (!$nick == "") { + $r = q("SELECT `nick` FROM `contact` WHERE `uid` = 0 AND `nurl` = '%s'", + dbesc(normalise_link($profile))); + if ($r) + $nick = $r[0]["nick"]; + } - if (!$nick == "") { - $diaspora = preg_replace("=https?://(.*)/u/(.*)=ism", "$2", $profile); - if ($diaspora != $profile) - $nick = $diaspora; - } + if (!$nick == "") { + $friendica = preg_replace("=https?://(.*)/profile/(.*)=ism", "$2", $profile); + if ($friendica != $profile) + $nick = $friendica; + } - if (!$nick == "") { - $twitter = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $profile); - if ($twitter != $profile) - $nick = $twitter; - } + if (!$nick == "") { + $diaspora = preg_replace("=https?://(.*)/u/(.*)=ism", "$2", $profile); + if ($diaspora != $profile) + $nick = $diaspora; + } + + if (!$nick == "") { + $twitter = preg_replace("=https?://twitter.com/(.*)=ism", "$1", $profile); + if ($twitter != $profile) + $nick = $twitter; + } - if (!$nick == "") { - $StatusnetHost = preg_replace("=https?://(.*)/user/(.*)=ism", "$1", $profile); - if ($StatusnetHost != $profile) { - $StatusnetUser = preg_replace("=https?://(.*)/user/(.*)=ism", "$2", $profile); - if ($StatusnetUser != $profile) { - $UserData = fetch_url("http://".$StatusnetHost."/api/users/show.json?user_id=".$StatusnetUser); - $user = json_decode($UserData); - if ($user) - $nick = $user->screen_name; + if (!$nick == "") { + $StatusnetHost = preg_replace("=https?://(.*)/user/(.*)=ism", "$1", $profile); + if ($StatusnetHost != $profile) { + $StatusnetUser = preg_replace("=https?://(.*)/user/(.*)=ism", "$2", $profile); + if ($StatusnetUser != $profile) { + $UserData = fetch_url("http://".$StatusnetHost."/api/users/show.json?user_id=".$StatusnetUser); + $user = json_decode($UserData); + if ($user) + $nick = $user->screen_name; + } } } + + // To-Do: look at the page if its really a pumpio site + //if (!$nick == "") { + // $pumpio = preg_replace("=https?://(.*)/(.*)/=ism", "$2", $profile."/"); + // if ($pumpio != $profile) + // $nick = $pumpio; + //
+ + //} + + if ($nick != "") + return($nick); + + return(false); } - // To-Do: look at the page if its really a pumpio site - //if (!$nick == "") { - // $pumpio = preg_replace("=https?://(.*)/(.*)/=ism", "$2", $profile."/"); - // if ($pumpio != $profile) - // $nick = $pumpio; - //
+ function api_in_reply_to($item) { + $in_reply_to = array(); - //} + $in_reply_to['status_id'] = NULL; + $in_reply_to['user_id'] = NULL; + $in_reply_to['status_id_str'] = NULL; + $in_reply_to['user_id_str'] = NULL; + $in_reply_to['screen_name'] = NULL; - if ($nick != "") { - q("UPDATE `unique_contacts` SET `nick` = '%s' WHERE `nick` != '%s' AND url = '%s'", - dbesc($nick), dbesc($nick), dbesc(normalise_link($profile))); - return($nick); - } + if (($item['thr-parent'] != $item['uri']) AND (intval($item['parent']) != intval($item['id']))) { + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' LIMIT 1", + intval($item['uid']), + dbesc($item['thr-parent'])); - return(false); -} + if (dbm::is_result($r)) { + $in_reply_to['status_id'] = intval($r[0]['id']); + } else { + $in_reply_to['status_id'] = intval($item['parent']); + } -function api_clean_plain_items($Text) { - $include_entities = strtolower(x($_REQUEST,'include_entities')?$_REQUEST['include_entities']:"false"); + $in_reply_to['status_id_str'] = (string) intval($in_reply_to['status_id']); - $Text = bb_CleanPictureLinks($Text); + $r = q("SELECT `contact`.`nick`, `contact`.`name`, `contact`.`id`, `contact`.`url` FROM item + STRAIGHT_JOIN `contact` ON `contact`.`id` = `item`.`author-id` + WHERE `item`.`id` = %d LIMIT 1", + intval($in_reply_to['status_id']) + ); - $URLSearchString = "^\[\]"; + if (dbm::is_result($r)) { + if ($r[0]['nick'] == "") { + $r[0]['nick'] = api_get_nick($r[0]["url"]); + } - $Text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'$1$3',$Text); + $in_reply_to['screen_name'] = (($r[0]['nick']) ? $r[0]['nick'] : $r[0]['name']); + $in_reply_to['user_id'] = intval($r[0]['id']); + $in_reply_to['user_id_str'] = (string) intval($r[0]['id']); + } - if ($include_entities == "true") { - $Text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$Text); - } - - $Text = preg_replace_callback("((.*?)\[class=(.*?)\](.*?)\[\/class\])ism","api_cleanup_share",$Text); - return($Text); -} - -function api_cleanup_share($shared) { - if ($shared[2] != "type-link") - return($shared[0]); - - if (!preg_match_all("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism",$shared[3], $bookmark)) - return($shared[0]); - - $title = ""; - $link = ""; - - if (isset($bookmark[2][0])) - $title = $bookmark[2][0]; - - if (isset($bookmark[1][0])) - $link = $bookmark[1][0]; - - if (strpos($shared[1],$title) !== false) - $title = ""; - - if (strpos($shared[1],$link) !== false) - $link = ""; - - $text = trim($shared[1]); - - //if (strlen($text) < strlen($title)) - if (($text == "") AND ($title != "")) - $text .= "\n\n".trim($title); - - if ($link != "") - $text .= "\n".trim($link); - - return(trim($text)); -} - -function api_best_nickname(&$contacts) { - $best_contact = array(); - - if (count($contact) == 0) - return; - - foreach ($contacts AS $contact) - if ($contact["network"] == "") { - $contact["network"] = "dfrn"; - $best_contact = array($contact); + // There seems to be situation, where both fields are identical: + // https://github.com/friendica/friendica/issues/1010 + // This is a bugfix for that. + if (intval($in_reply_to['status_id']) == intval($item['id'])) { + logger('this message should never appear: id: '.$item['id'].' similar to reply-to: '.$in_reply_to['status_id'], LOGGER_DEBUG); + $in_reply_to['status_id'] = NULL; + $in_reply_to['user_id'] = NULL; + $in_reply_to['status_id_str'] = NULL; + $in_reply_to['user_id_str'] = NULL; + $in_reply_to['screen_name'] = NULL; + } } - if (sizeof($best_contact) == 0) + return $in_reply_to; + } + + function api_clean_plain_items($Text) { + $include_entities = strtolower(x($_REQUEST,'include_entities')?$_REQUEST['include_entities']:"false"); + + $Text = bb_CleanPictureLinks($Text); + $URLSearchString = "^\[\]"; + + $Text = preg_replace("/([!#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'$1$3',$Text); + + if ($include_entities == "true") { + $Text = preg_replace("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism",'[url=$1]$1[/url]',$Text); + } + + // Simplify "attachment" element + $Text = api_clean_attachments($Text); + + return($Text); + } + + /** + * @brief Removes most sharing information for API text export + * + * @param string $body The original body + * + * @return string Cleaned body + */ + function api_clean_attachments($body) { + $data = get_attachment_data($body); + + if (!$data) + return $body; + + $body = ""; + + if (isset($data["text"])) + $body = $data["text"]; + + if (($body == "") AND (isset($data["title"]))) + $body = $data["title"]; + + if (isset($data["url"])) + $body .= "\n".$data["url"]; + + $body .= $data["after"]; + + return $body; + } + + function api_best_nickname(&$contacts) { + $best_contact = array(); + + if (count($contact) == 0) + return; + foreach ($contacts AS $contact) - if ($contact["network"] == "dfrn") + if ($contact["network"] == "") { + $contact["network"] = "dfrn"; $best_contact = array($contact); + } - if (sizeof($best_contact) == 0) - foreach ($contacts AS $contact) - if ($contact["network"] == "dspr") - $best_contact = array($contact); + if (sizeof($best_contact) == 0) + foreach ($contacts AS $contact) + if ($contact["network"] == "dfrn") + $best_contact = array($contact); - if (sizeof($best_contact) == 0) - foreach ($contacts AS $contact) - if ($contact["network"] == "stat") - $best_contact = array($contact); + if (sizeof($best_contact) == 0) + foreach ($contacts AS $contact) + if ($contact["network"] == "dspr") + $best_contact = array($contact); - if (sizeof($best_contact) == 0) - foreach ($contacts AS $contact) - if ($contact["network"] == "pump") - $best_contact = array($contact); + if (sizeof($best_contact) == 0) + foreach ($contacts AS $contact) + if ($contact["network"] == "stat") + $best_contact = array($contact); - if (sizeof($best_contact) == 0) - foreach ($contacts AS $contact) - if ($contact["network"] == "twit") - $best_contact = array($contact); + if (sizeof($best_contact) == 0) + foreach ($contacts AS $contact) + if ($contact["network"] == "pump") + $best_contact = array($contact); - if (sizeof($best_contact) == 1) - $contacts = $best_contact; - else - $contacts = array($contacts[0]); -} + if (sizeof($best_contact) == 0) + foreach ($contacts AS $contact) + if ($contact["network"] == "twit") + $best_contact = array($contact); + if (sizeof($best_contact) == 1) + $contacts = $best_contact; + else + $contacts = array($contacts[0]); + } + + // return all or a specified group of the user with the containing contacts + function api_friendica_group_show($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $gid = (x($_REQUEST,'gid') ? $_REQUEST['gid'] : 0); + $uid = $user_info['uid']; + + // get data of the specified group id or all groups if not specified + if ($gid != 0) { + $r = q("SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d AND `id` = %d", + intval($uid), + intval($gid)); + // error message if specified gid is not in database + if (!dbm::is_result($r)) + throw new BadRequestException("gid not available"); + } + else + $r = q("SELECT * FROM `group` WHERE `deleted` = 0 AND `uid` = %d", + intval($uid)); + + // loop through all groups and retrieve all members for adding data in the user array + foreach ($r as $rr) { + $members = group_get_members($rr['id']); + $users = array(); + + if ($type == "xml") { + $user_element = "users"; + $k = 0; + foreach ($members as $member) { + $user = api_get_user($a, $member['nurl']); + $users[$k++.":user"] = $user; + } + } else { + $user_element = "user"; + foreach ($members as $member) { + $user = api_get_user($a, $member['nurl']); + $users[] = $user; + } + } + $grps[] = array('name' => $rr['name'], 'gid' => $rr['id'], $user_element => $users); + } + return api_format_data("groups", $type, array('group' => $grps)); + } + api_register_func('api/friendica/group_show', 'api_friendica_group_show', true); + + + // delete the specified group of the user + function api_friendica_group_delete($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $gid = (x($_REQUEST,'gid') ? $_REQUEST['gid'] : 0); + $name = (x($_REQUEST, 'name') ? $_REQUEST['name'] : ""); + $uid = $user_info['uid']; + + // error if no gid specified + if ($gid == 0 || $name == "") + throw new BadRequestException('gid or name not specified'); + + // get data of the specified group id + $r = q("SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d", + intval($uid), + intval($gid)); + // error message if specified gid is not in database + if (!dbm::is_result($r)) + throw new BadRequestException('gid not available'); + + // get data of the specified group id and group name + $rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `id` = %d AND `name` = '%s'", + intval($uid), + intval($gid), + dbesc($name)); + // error message if specified gid is not in database + if (!dbm::is_result($rname)) + throw new BadRequestException('wrong group name'); + + // delete group + $ret = group_rmv($uid, $name); + if ($ret) { + // return success + $success = array('success' => $ret, 'gid' => $gid, 'name' => $name, 'status' => 'deleted', 'wrong users' => array()); + return api_format_data("group_delete", $type, array('result' => $success)); + } + else + throw new BadRequestException('other API error'); + } + api_register_func('api/friendica/group_delete', 'api_friendica_group_delete', true, API_METHOD_DELETE); + + + // create the specified group with the posted array of contacts + function api_friendica_group_create($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $name = (x($_REQUEST, 'name') ? $_REQUEST['name'] : ""); + $uid = $user_info['uid']; + $json = json_decode($_POST['json'], true); + $users = $json['user']; + + // error if no name specified + if ($name == "") + throw new BadRequestException('group name not specified'); + + // get data of the specified group name + $rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 0", + intval($uid), + dbesc($name)); + // error message if specified group name already exists + if (dbm::is_result($rname)) + throw new BadRequestException('group name already exists'); + + // check if specified group name is a deleted group + $rname = q("SELECT * FROM `group` WHERE `uid` = %d AND `name` = '%s' AND `deleted` = 1", + intval($uid), + dbesc($name)); + // error message if specified group name already exists + if (dbm::is_result($rname)) + $reactivate_group = true; + + // create group + $ret = group_add($uid, $name); + if ($ret) + $gid = group_byname($uid, $name); + else + throw new BadRequestException('other API error'); + + // add members + $erroraddinguser = false; + $errorusers = array(); + foreach ($users as $user) { + $cid = $user['cid']; + // check if user really exists as contact + $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d", + intval($cid), + intval($uid)); + if (count($contact)) + $result = group_add_member($uid, $name, $cid, $gid); + else { + $erroraddinguser = true; + $errorusers[] = $cid; + } + } + + // return success message incl. missing users in array + $status = ($erroraddinguser ? "missing user" : ($reactivate_group ? "reactivated" : "ok")); + $success = array('success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers); + return api_format_data("group_create", $type, array('result' => $success)); + } + api_register_func('api/friendica/group_create', 'api_friendica_group_create', true, API_METHOD_POST); + + + // update the specified group with the posted array of contacts + function api_friendica_group_update($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $uid = $user_info['uid']; + $gid = (x($_REQUEST, 'gid') ? $_REQUEST['gid'] : 0); + $name = (x($_REQUEST, 'name') ? $_REQUEST['name'] : ""); + $json = json_decode($_POST['json'], true); + $users = $json['user']; + + // error if no name specified + if ($name == "") + throw new BadRequestException('group name not specified'); + + // error if no gid specified + if ($gid == "") + throw new BadRequestException('gid not specified'); + + // remove members + $members = group_get_members($gid); + foreach ($members as $member) { + $cid = $member['id']; + foreach ($users as $user) { + $found = ($user['cid'] == $cid ? true : false); + } + if (!$found) { + $ret = group_rmv_member($uid, $name, $cid); + } + } + + // add members + $erroraddinguser = false; + $errorusers = array(); + foreach ($users as $user) { + $cid = $user['cid']; + // check if user really exists as contact + $contact = q("SELECT * FROM `contact` WHERE `id` = %d AND `uid` = %d", + intval($cid), + intval($uid)); + if (count($contact)) + $result = group_add_member($uid, $name, $cid, $gid); + else { + $erroraddinguser = true; + $errorusers[] = $cid; + } + } + + // return success message incl. missing users in array + $status = ($erroraddinguser ? "missing user" : "ok"); + $success = array('success' => true, 'gid' => $gid, 'name' => $name, 'status' => $status, 'wrong users' => $errorusers); + return api_format_data("group_update", $type, array('result' => $success)); + } + api_register_func('api/friendica/group_update', 'api_friendica_group_update', true, API_METHOD_POST); + + + function api_friendica_activity($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + $verb = strtolower($a->argv[3]); + $verb = preg_replace("|\..*$|", "", $verb); + + $id = (x($_REQUEST, 'id') ? $_REQUEST['id'] : 0); + + $res = do_like($id, $verb); + + if ($res) { + if ($type == "xml") + $ok = "true"; + else + $ok = "ok"; + return api_format_data('ok', $type, array('ok' => $ok)); + } else { + throw new BadRequestException('Error adding activity'); + } + + } + api_register_func('api/friendica/activity/like', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/dislike', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/attendyes', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/attendno', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/attendmaybe', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/unlike', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/undislike', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/unattendyes', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/unattendno', 'api_friendica_activity', true, API_METHOD_POST); + api_register_func('api/friendica/activity/unattendmaybe', 'api_friendica_activity', true, API_METHOD_POST); + + /** + * @brief Returns notifications + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string + */ + function api_friendica_notification($type) { + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + if ($a->argc!==3) throw new BadRequestException("Invalid argument count"); + $nm = new NotificationsManager(); + + $notes = $nm->getAll(array(), "+seen -date", 50); + + if ($type == "xml") { + $xmlnotes = array(); + foreach ($notes AS $note) + $xmlnotes[] = array("@attributes" => $note); + + $notes = $xmlnotes; + } + + return api_format_data("notes", $type, array('note' => $notes)); + } + + /** + * @brief Set notification as seen and returns associated item (if possible) + * + * POST request with 'id' param as notification id + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string + */ + function api_friendica_notification_seen($type){ + + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + if ($a->argc!==4) throw new BadRequestException("Invalid argument count"); + + $id = (x($_REQUEST, 'id') ? intval($_REQUEST['id']) : 0); + + $nm = new NotificationsManager(); + $note = $nm->getByID($id); + if (is_null($note)) throw new BadRequestException("Invalid argument"); + + $nm->setSeen($note); + if ($note['otype']=='item') { + // would be really better with an ItemsManager and $im->getByID() :-P + $r = q("SELECT * FROM `item` WHERE `id`=%d AND `uid`=%d", + intval($note['iid']), + intval(local_user()) + ); + if ($r!==false) { + // we found the item, return it to the user + $user_info = api_get_user($a); + $ret = api_format_items($r,$user_info, false, $type); + $data = array('status' => $ret); + return api_format_data("status", $type, $data); + } + // the item can't be found, but we set the note as seen, so we count this as a success + } + return api_format_data('result', $type, array('result' => "success")); + } + + api_register_func('api/friendica/notification/seen', 'api_friendica_notification_seen', true, API_METHOD_POST); + api_register_func('api/friendica/notification', 'api_friendica_notification', true, API_METHOD_GET); + + + /** + * @brief update a direct_message to seen state + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string (success result=ok, error result=error with error message) + */ + function api_friendica_direct_messages_setseen($type){ + $a = get_app(); + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $uid = $user_info['uid']; + $id = (x($_REQUEST, 'id') ? $_REQUEST['id'] : 0); + + // return error if id is zero + if ($id == "") { + $answer = array('result' => 'error', 'message' => 'message id not specified'); + return api_format_data("direct_messages_setseen", $type, array('$result' => $answer)); + } + + // get data of the specified message id + $r = q("SELECT `id` FROM `mail` WHERE `id` = %d AND `uid` = %d", + intval($id), + intval($uid)); + // error message if specified id is not in database + if (!dbm::is_result($r)) { + $answer = array('result' => 'error', 'message' => 'message id not in database'); + return api_format_data("direct_messages_setseen", $type, array('$result' => $answer)); + } + + // update seen indicator + $result = q("UPDATE `mail` SET `seen` = 1 WHERE `id` = %d AND `uid` = %d", + intval($id), + intval($uid)); + + if ($result) { + // return success + $answer = array('result' => 'ok', 'message' => 'message set to seen'); + return api_format_data("direct_message_setseen", $type, array('$result' => $answer)); + } else { + $answer = array('result' => 'error', 'message' => 'unknown error'); + return api_format_data("direct_messages_setseen", $type, array('$result' => $answer)); + } + } + api_register_func('api/friendica/direct_messages_setseen', 'api_friendica_direct_messages_setseen', true); + + + + + /** + * @brief search for direct_messages containing a searchstring through api + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string (success: success=true if found and search_result contains found messages + * success=false if nothing was found, search_result='nothing found', + * error: result=error with error message) + */ + function api_friendica_direct_messages_search($type){ + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // params + $user_info = api_get_user($a); + $searchstring = (x($_REQUEST,'searchstring') ? $_REQUEST['searchstring'] : ""); + $uid = $user_info['uid']; + + // error if no searchstring specified + if ($searchstring == "") { + $answer = array('result' => 'error', 'message' => 'searchstring not specified'); + return api_format_data("direct_messages_search", $type, array('$result' => $answer)); + } + + // get data for the specified searchstring + $r = q("SELECT `mail`.*, `contact`.`nurl` AS `contact-url` FROM `mail`,`contact` WHERE `mail`.`contact-id` = `contact`.`id` AND `mail`.`uid`=%d AND `body` LIKE '%s' ORDER BY `mail`.`id` DESC", + intval($uid), + dbesc('%'.$searchstring.'%') + ); + + $profile_url = $user_info["url"]; + // message if nothing was found + if (!dbm::is_result($r)) + $success = array('success' => false, 'search_results' => 'problem with query'); + else if (count($r) == 0) + $success = array('success' => false, 'search_results' => 'nothing found'); + else { + $ret = Array(); + foreach($r as $item) { + if ($box == "inbox" || $item['from-url'] != $profile_url){ + $recipient = $user_info; + $sender = api_get_user($a,normalise_link($item['contact-url'])); + } + elseif ($box == "sentbox" || $item['from-url'] == $profile_url){ + $recipient = api_get_user($a,normalise_link($item['contact-url'])); + $sender = $user_info; + } + $ret[]=api_format_messages($item, $recipient, $sender); + } + $success = array('success' => true, 'search_results' => $ret); + } + + return api_format_data("direct_message_search", $type, array('$result' => $success)); + } + api_register_func('api/friendica/direct_messages_search', 'api_friendica_direct_messages_search', true); + + /** + * @brief return data of all the profiles a user has to the client + * + * @param string $type Known types are 'atom', 'rss', 'xml' and 'json' + * @return string + */ + function api_friendica_profile_show($type){ + $a = get_app(); + + if (api_user()===false) throw new ForbiddenException(); + + // input params + $profileid = (x($_REQUEST,'profile_id') ? $_REQUEST['profile_id'] : 0); + + // retrieve general information about profiles for user + $multi_profiles = feature_enabled(api_user(),'multi_profiles'); + $directory = get_config('system', 'directory'); + +// get data of the specified profile id or all profiles of the user if not specified + if ($profileid != 0) { + $r = q("SELECT * FROM `profile` WHERE `uid` = %d AND `id` = %d", + intval(api_user()), + intval($profileid)); + // error message if specified gid is not in database + if (!dbm::is_result($r)) + throw new BadRequestException("profile_id not available"); + } + else + $r = q("SELECT * FROM `profile` WHERE `uid` = %d", + intval(api_user())); + + // loop through all returned profiles and retrieve data and users + $k = 0; + foreach ($r as $rr) { + $profile = api_format_items_profiles($rr, $type); + + // select all users from contact table, loop and prepare standard return for user data + $users = array(); + $r = q("SELECT `id`, `nurl` FROM `contact` WHERE `uid`= %d AND `profile-id` = %d", + intval(api_user()), + intval($rr['profile_id'])); + + foreach ($r as $rr) { + $user = api_get_user($a, $rr['nurl']); + ($type == "xml") ? $users[$k++.":user"] = $user : $users[] = $user; + } + $profile['users'] = $users; + + // add prepared profile data to array for final return + if ($type == "xml") { + $profiles[$k++.":profile"] = $profile; + } else { + $profiles[] = $profile; + } + } + + // return settings, authenticated user and profiles data + $result = array('multi_profiles' => $multi_profiles ? true : false, + 'global_dir' => $directory, + 'friendica_owner' => api_get_user($a, intval(api_user())), + 'profiles' => $profiles); + return api_format_data("friendica_profiles", $type, array('$result' => $result)); + } + api_register_func('api/friendica/profile/show', 'api_friendica_profile_show', true, API_METHOD_GET); /* To.Do: @@ -3024,7 +4025,7 @@ To.Do: [include_rts] => 1 [include_reply_count] => true [include_descendent_reply_count] => true - +(?) Not implemented by now: @@ -3038,6 +4039,9 @@ account/update_profile_background_image account/update_profile_image blocks/create blocks/destroy +friendica/profile/update +friendica/profile/create +friendica/profile/delete Not implemented in status.net: statuses/retweeted_to_me diff --git a/include/auth.php b/include/auth.php index 4c695cc1e3..e3c8d92eeb 100644 --- a/include/auth.php +++ b/include/auth.php @@ -1,72 +1,79 @@ uid)) { + $r = q("SELECT `user`.*, `user`.`pubkey` as `upubkey`, `user`.`prvkey` as `uprvkey` + FROM `user` WHERE `uid` = %d AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` LIMIT 1", + intval($data->uid) + ); - unset($_SESSION['authenticated']); - unset($_SESSION['uid']); - unset($_SESSION['visitor_id']); - unset($_SESSION['administrator']); - unset($_SESSION['cid']); - unset($_SESSION['theme']); - unset($_SESSION['mobile-theme']); - unset($_SESSION['page_flags']); - unset($_SESSION['submanage']); - unset($_SESSION['my_url']); - unset($_SESSION['my_address']); - unset($_SESSION['addr']); - unset($_SESSION['return_url']); + if ($r) { + if ($data->hash != cookie_hash($r[0])) { + logger("Hash for user ".$data->uid." doesn't fit."); + nuke_session(); + goaway(z_root()); + } + + // Renew the cookie + new_cookie(604800, $r[0]); + + // Do the authentification if not done by now + if (!isset($_SESSION) OR !isset($_SESSION['authenticated'])) { + authenticate_success($r[0]); + + if (get_config('system','paranoia')) + $_SESSION['addr'] = $data->ip; + } + } + } } -// login/logout +// login/logout +if (isset($_SESSION) && x($_SESSION,'authenticated') && (!x($_POST,'auth-params') || ($_POST['auth-params'] !== 'login'))) { + if ((x($_POST,'auth-params') && ($_POST['auth-params'] === 'logout')) || ($a->module === 'logout')) { - -if((isset($_SESSION)) && (x($_SESSION,'authenticated')) && ((! (x($_POST,'auth-params'))) || ($_POST['auth-params'] !== 'login'))) { - - if(((x($_POST,'auth-params')) && ($_POST['auth-params'] === 'logout')) || ($a->module === 'logout')) { - // process logout request call_hooks("logging_out"); nuke_session(); - info( t('Logged out.') . EOL); + info(t('Logged out.').EOL); goaway(z_root()); } - if(x($_SESSION,'visitor_id') && (! x($_SESSION,'uid'))) { + if (x($_SESSION,'visitor_id') && !x($_SESSION,'uid')) { $r = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1", intval($_SESSION['visitor_id']) ); - if(count($r)) { + if (dbm::is_result($r)) { $a->contact = $r[0]; } } - if(x($_SESSION,'uid')) { + if (x($_SESSION,'uid')) { // already logged in user returning $check = get_config('system','paranoia'); // extra paranoia - if the IP changed, log them out - if($check && ($_SESSION['addr'] != $_SERVER['REMOTE_ADDR'])) { - logger('Session address changed. Paranoid setting in effect, blocking session. ' - . $_SESSION['addr'] . ' != ' . $_SERVER['REMOTE_ADDR']); + if ($check && ($_SESSION['addr'] != $_SERVER['REMOTE_ADDR'])) { + logger('Session address changed. Paranoid setting in effect, blocking session. '. + $_SESSION['addr'].' != '.$_SERVER['REMOTE_ADDR']); nuke_session(); goaway(z_root()); } - $r = q("SELECT `user`.*, `user`.`pubkey` as `upubkey`, `user`.`prvkey` as `uprvkey` - FROM `user` WHERE `uid` = %d AND `blocked` = 0 AND `account_expired` = 0 AND `account_removed` = 0 AND `verified` = 1 LIMIT 1", + $r = q("SELECT `user`.*, `user`.`pubkey` as `upubkey`, `user`.`prvkey` as `uprvkey` + FROM `user` WHERE `uid` = %d AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` LIMIT 1", intval($_SESSION['uid']) ); - if(! count($r)) { + if (!dbm::is_result($r)) { nuke_session(); goaway(z_root()); } @@ -74,32 +81,29 @@ if((isset($_SESSION)) && (x($_SESSION,'authenticated')) && ((! (x($_POST,'auth-p // Make sure to refresh the last login time for the user if the user // stays logged in for a long time, e.g. with "Remember Me" $login_refresh = false; - if(! x($_SESSION['last_login_date'])) { + if (!x($_SESSION['last_login_date'])) { $_SESSION['last_login_date'] = datetime_convert('UTC','UTC'); } - if( strcmp(datetime_convert('UTC','UTC','now - 12 hours'), $_SESSION['last_login_date']) > 0 ) { + if (strcmp(datetime_convert('UTC','UTC','now - 12 hours'), $_SESSION['last_login_date']) > 0) { $_SESSION['last_login_date'] = datetime_convert('UTC','UTC'); $login_refresh = true; } authenticate_success($r[0], false, false, $login_refresh); } -} -else { +} else { - if(isset($_SESSION)) { - nuke_session(); - } + session_unset(); - if((x($_POST,'password')) && strlen($_POST['password'])) + if (x($_POST,'password') && strlen($_POST['password'])) $encrypted = hash('whirlpool',trim($_POST['password'])); else { - if((x($_POST,'openid_url')) && strlen($_POST['openid_url']) || + if ((x($_POST,'openid_url')) && strlen($_POST['openid_url']) || (x($_POST,'username')) && strlen($_POST['username'])) { $noid = get_config('system','no_openid'); - $openid_url = trim((strlen($_POST['openid_url'])?$_POST['openid_url']:$_POST['username']) ); + $openid_url = trim((strlen($_POST['openid_url'])?$_POST['openid_url']:$_POST['username'])); // validate_url alters the calling parameter @@ -107,36 +111,35 @@ else { // if it's an email address or doesn't resolve to a URL, fail. - if(($noid) || (strpos($temp_string,'@')) || (! validate_url($temp_string))) { + if ($noid || strpos($temp_string,'@') || !validate_url($temp_string)) { $a = get_app(); - notice( t('Login failed.') . EOL); + notice(t('Login failed.').EOL); goaway(z_root()); // NOTREACHED } // Otherwise it's probably an openid. - try { - require_once('library/openid.php'); - $openid = new LightOpenID; - $openid->identity = $openid_url; - $_SESSION['openid'] = $openid_url; - $a = get_app(); - $openid->returnUrl = $a->get_baseurl(true) . '/openid'; - goaway($openid->authUrl()); - } catch (Exception $e) { - notice( t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.').'

'. t('The error message was:').' '.$e->getMessage()); - } + try { + require_once('library/openid.php'); + $openid = new LightOpenID; + $openid->identity = $openid_url; + $_SESSION['openid'] = $openid_url; + $openid->returnUrl = App::get_baseurl(true).'/openid'; + goaway($openid->authUrl()); + } catch (Exception $e) { + notice(t('We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID.').'

'.t('The error message was:').' '.$e->getMessage()); + } // NOTREACHED } } - if((x($_POST,'auth-params')) && $_POST['auth-params'] === 'login') { + if (x($_POST,'auth-params') && $_POST['auth-params'] === 'login') { $record = null; $addon_auth = array( - 'username' => trim($_POST['username']), + 'username' => trim($_POST['username']), 'password' => trim($_POST['password']), 'authenticated' => 0, 'user_record' => null @@ -152,48 +155,37 @@ else { call_hooks('authenticate', $addon_auth); - if(($addon_auth['authenticated']) && (count($addon_auth['user_record']))) { + if ($addon_auth['authenticated'] && count($addon_auth['user_record'])) $record = $addon_auth['user_record']; - } else { // process normal login request - $r = q("SELECT `user`.*, `user`.`pubkey` as `upubkey`, `user`.`prvkey` as `uprvkey` - FROM `user` WHERE ( `email` = '%s' OR `nickname` = '%s' ) - AND `password` = '%s' AND `blocked` = 0 AND `account_expired` = 0 AND `account_removed` = 0 AND `verified` = 1 LIMIT 1", + $r = q("SELECT `user`.*, `user`.`pubkey` as `upubkey`, `user`.`prvkey` as `uprvkey` + FROM `user` WHERE (`email` = '%s' OR `nickname` = '%s') + AND `password` = '%s' AND NOT `blocked` AND NOT `account_expired` AND NOT `account_removed` AND `verified` LIMIT 1", dbesc(trim($_POST['username'])), dbesc(trim($_POST['username'])), dbesc($encrypted) ); - if(count($r)) + if (dbm::is_result($r)) $record = $r[0]; } - if((! $record) || (! count($record))) { - logger('authenticate: failed login attempt: ' . notags(trim($_POST['username'])) . ' from IP ' . $_SERVER['REMOTE_ADDR']); - notice( t('Login failed.') . EOL ); + if (!$record || !count($record)) { + logger('authenticate: failed login attempt: '.notags(trim($_POST['username'])).' from IP '.$_SERVER['REMOTE_ADDR']); + notice(t('Login failed.').EOL); goaway(z_root()); - } + } - // If the user specified to remember the authentication, then change the cookie - // to expire after one year (the default is when the browser is closed). - // If the user did not specify to remember, change the cookie to expire when the - // browser is closed. The reason this is necessary is because if the user - // specifies to remember, then logs out and logs back in without specifying to - // remember, the old "remember" cookie may remain and prevent the session from - // expiring when the browser is closed. - // - // It seems like I should be able to test for the old cookie, but for some reason when - // I read the lifetime value from session_get_cookie_params(), I always get '0' - // (i.e. expire when the browser is closed), even when there's a time expiration - // on the cookie - if($_POST['remember']) { - new_cookie(31449600); // one year - } - else { + // If the user specified to remember the authentication, then set a cookie + // that expires after one week (the default is when the browser is closed). + // The cookie will be renewed automatically. + // The week ensures that sessions will expire after some inactivity. + if ($_POST['remember']) + new_cookie(604800, $r[0]); + else new_cookie(0); // 0 means delete on browser exit - } // if we haven't failed up this point, log them in. @@ -202,10 +194,48 @@ else { } } -function new_cookie($time) { - $old_sid = session_id(); - session_set_cookie_params("$time"); - session_regenerate_id(false); +/** + * @brief Kills the "Friendica" cookie and all session data + */ +function nuke_session() { + + new_cookie(-3600); // make sure cookie is deleted on browser close, as a security measure + session_unset(); + session_destroy(); +} + +/** + * @brief Calculate the hash that is needed for the "Friendica" cookie + * + * @param array $user Record from "user" table + * + * @return string Hashed data + */ +function cookie_hash($user) { + return(hash("sha256", get_config("system", "site_prvkey"). + $user["uprvkey"]. + $user["password"])); +} + +/** + * @brief Set the "Friendica" cookie + * + * @param int $time + * @param array $user Record from "user" table + */ +function new_cookie($time, $user = array()) { + + if ($time != 0) + $time = $time + time(); + + if ($user) + $value = json_encode(array("uid" => $user["uid"], + "hash" => cookie_hash($user), + "ip" => $_SERVER['REMOTE_ADDR'])); + else + $value = ""; + + setcookie("Friendica", $value, $time, "/", "", + (get_config('system', 'ssl_policy') == SSL_POLICY_FULL), true); - q("UPDATE session SET sid = '%s' WHERE sid = '%s'", dbesc(session_id()), dbesc($old_sid)); } diff --git a/include/auth_ejabberd.php b/include/auth_ejabberd.php index 9a9d9accad..8ee3af8e2b 100755 --- a/include/auth_ejabberd.php +++ b/include/auth_ejabberd.php @@ -47,11 +47,10 @@ require_once("boot.php"); global $a, $db; -if(is_null($a)) { +if (is_null($a)) $a = new App; -} -if(is_null($db)) { +if (is_null($db)) { @include(".htconfig.php"); require_once("include/dba.php"); $db = new dba($db_host, $db_user, $db_pass, $db_data); @@ -66,162 +65,271 @@ $bDebug = get_config('jabber','debug'); $oAuth = new exAuth($sLogFile, $bDebug); -class exAuth -{ +class exAuth { private $sLogFile; private $bDebug; private $rLogFile; - public function __construct($sLogFile, $bDebug) - { + /** + * @brief Create the class and do the authentification studd + * + * @param string $sLogFile The logfile name + * @param boolean $bDebug Debug mode + */ + public function __construct($sLogFile, $bDebug) { global $db; // setter $this->sLogFile = $sLogFile; $this->bDebug = $bDebug; - // ovo ne provjeravamo jer ako ne mozes kreirati log file, onda si u kvascu :) + // Open the logfile if the logfile name is defined if ($this->sLogFile != '') $this->rLogFile = fopen($this->sLogFile, "a") or die("Error opening log file: ". $this->sLogFile); $this->writeLog("[exAuth] start"); - // ovdje bi trebali biti spojeni na MySQL, imati otvoren log i zavrtit cekalicu + // We are connected to the SQL server and are having a log file. do { - $iHeader = fgets(STDIN, 3); - $aLength = unpack("n", $iHeader); - $iLength = $aLength["1"]; - if($iLength > 0) { - // ovo znaci da smo nesto dobili - $sData = fgets(STDIN, $iLength + 1); - $this->writeDebugLog("[debug] received data: ". $sData); - $aCommand = explode(":", $sData); - if (is_array($aCommand)){ - switch ($aCommand[0]){ - case "isuser": - // provjeravamo je li korisnik dobar - if (!isset($aCommand[1])){ - $this->writeLog("[exAuth] invalid isuser command, no username given"); - fwrite(STDOUT, pack("nn", 2, 0)); - } else { - // ovdje provjeri je li korisnik OK - $sUser = str_replace(array("%20", "(a)"), array(" ", "@"), $aCommand[1]); - $this->writeDebugLog("[debug] checking isuser for ". $sUser); - $sQuery = "SELECT `uid` FROM `user` WHERE `nickname`='". $db->escape($sUser) ."'"; - $this->writeDebugLog("[debug] using query ". $sQuery); - if ($oResult = q($sQuery)){ - if ($oResult) { - // korisnik OK - $this->writeLog("[exAuth] valid user: ". $sUser); - fwrite(STDOUT, pack("nn", 2, 1)); - } else { - // korisnik nije OK - $this->writeLog("[exAuth] invalid user: ". $sUser); - fwrite(STDOUT, pack("nn", 2, 0)); - } - //$oResult->close(); - } else { - $this->writeLog("[MySQL] invalid query: ". $sQuery); - fwrite(STDOUT, pack("nn", 2, 0)); - } - } - break; - case "auth": - // provjeravamo autentifikaciju korisnika - if (sizeof($aCommand) != 4){ - $this->writeLog("[exAuth] invalid auth command, data missing"); - fwrite(STDOUT, pack("nn", 2, 0)); - } else { - // ovdje provjeri prijavu - $sUser = str_replace(array("%20", "(a)"), array(" ", "@"), $aCommand[1]); - $this->writeDebugLog("[debug] doing auth for ". $sUser); - //$sQuery = "SELECT `uid`, `password` FROM `user` WHERE `password`='".hash('whirlpool',$aCommand[3])."' AND `nickname`='". $db->escape($sUser) ."'"; - $sQuery = "SELECT `uid`, `password` FROM `user` WHERE `nickname`='". $db->escape($sUser) ."'"; - $this->writeDebugLog("[debug] using query ". $sQuery); - if ($oResult = q($sQuery)){ - $uid = $oResult[0]["uid"]; - $Error = ($oResult[0]["password"] != hash('whirlpool',$aCommand[3])); -/* - if ($oResult[0]["password"] == hash('whirlpool',$aCommand[3])) { - // korisnik OK - $this->writeLog("[exAuth] authentificated user ". $sUser ."@". $aCommand[2]); - fwrite(STDOUT, pack("nn", 2, 1)); - } else { - // korisnik nije OK - $this->writeLog("[exAuth] authentification failed for user ". $sUser ."@". $aCommand[2]); - fwrite(STDOUT, pack("nn", 2, 0)); - } - $oResult->close(); -*/ - } else { - $this->writeLog("[MySQL] invalid query: ". $sQuery); - $Error = true; - $uid = -1; - } - if ($Error) { - $oConfig = q("SELECT `v` FROM `pconfig` WHERE `uid`=%d AND `cat` = 'xmpp' AND `k`='password' LIMIT 1;", intval($uid)); - $this->writeLog("[exAuth] got password ".$oConfig[0]["v"]); - $Error = ($aCommand[3] != $oConfig[0]["v"]); - } - - if ($Error) { - $this->writeLog("[exAuth] authentification failed for user ". $sUser ."@". $aCommand[2]); - fwrite(STDOUT, pack("nn", 2, 0)); - } else { - $this->writeLog("[exAuth] authentificated user ". $sUser ."@". $aCommand[2]); - fwrite(STDOUT, pack("nn", 2, 1)); - } - } - break; - case "setpass": - // postavljanje zaporke, onemoguceno - $this->writeLog("[exAuth] setpass command disabled"); - fwrite(STDOUT, pack("nn", 2, 0)); - break; - default: - // ako je uhvaceno ista drugo - $this->writeLog("[exAuth] unknown command ". $aCommand[0]); - fwrite(STDOUT, pack("nn", 2, 0)); - break; - } - } else { - $this->writeDebugLog("[debug] invalid command string"); - fwrite(STDOUT, pack("nn", 2, 0)); - } + // Quit if the database connection went down + if (!$db->connected()) { + $this->writeDebugLog("[debug] the database connection went down"); + return; + } + + $iHeader = fgets(STDIN, 3); + $aLength = unpack("n", $iHeader); + $iLength = $aLength["1"]; + + // No data? Then quit + if ($iLength == 0) { + $this->writeDebugLog("[debug] we got no data"); + return; + } + + // Fetching the data + $sData = fgets(STDIN, $iLength + 1); + $this->writeDebugLog("[debug] received data: ". $sData); + $aCommand = explode(":", $sData); + if (is_array($aCommand)) { + switch ($aCommand[0]) { + case "isuser": + // Check the existance of a given username + $this->isuser($aCommand); + break; + case "auth": + // Check if the givven password is correct + $this->auth($aCommand); + break; + case "setpass": + // We don't accept the setting of passwords here + $this->writeLog("[exAuth] setpass command disabled"); + fwrite(STDOUT, pack("nn", 2, 0)); + break; + default: + // We don't know the given command + $this->writeLog("[exAuth] unknown command ". $aCommand[0]); + fwrite(STDOUT, pack("nn", 2, 0)); + break; + } + } else { + $this->writeDebugLog("[debug] invalid command string"); + fwrite(STDOUT, pack("nn", 2, 0)); } - unset ($iHeader); - unset ($aLength); - unset ($iLength); - unset($aCommand); } while (true); } - public function __destruct() - { - // zatvori log file + /** + * @brief Check if the given username exists + * + * @param array $aCommand The command array + */ + private function isuser($aCommand) { + $a = get_app(); + + // Check if there is a username + if (!isset($aCommand[1])) { + $this->writeLog("[exAuth] invalid isuser command, no username given"); + fwrite(STDOUT, pack("nn", 2, 0)); + return; + } + + // Now we check if the given user is valid + $sUser = str_replace(array("%20", "(a)"), array(" ", "@"), $aCommand[1]); + $this->writeDebugLog("[debug] checking isuser for ". $sUser."@".$aCommand[2]); + + // Does the hostname match? So we try directly + if ($a->get_hostname() == $aCommand[2]) { + $sQuery = "SELECT `uid` FROM `user` WHERE `nickname`='".dbesc($sUser)."'"; + $this->writeDebugLog("[debug] using query ". $sQuery); + $r = q($sQuery); + $found = dbm::is_result($r); + } else { + $found = false; + } + + // If the hostnames doesn't match or there is some failure, we try to check remotely + if (!$found) { + $found = $this->check_user($aCommand[2], $aCommand[1], true); + } + + if ($found) { + // The user is okay + $this->writeLog("[exAuth] valid user: ". $sUser); + fwrite(STDOUT, pack("nn", 2, 1)); + } else { + // The user isn't okay + $this->writeLog("[exAuth] invalid user: ". $sUser); + fwrite(STDOUT, pack("nn", 2, 0)); + } + } + + /** + * @brief Check remote user existance via HTTP(S) + * + * @param string $host The hostname + * @param string $user Username + * @param boolean $ssl Should the check be done via SSL? + * + * @return boolean Was the user found? + */ + private function check_user($host, $user, $ssl) { + + $url = ($ssl ? "https":"http")."://".$host."/noscrape/".$user; + + $data = z_fetch_url($url); + + if (!is_array($data)) + return(false); + + if ($data["return_code"] != "200") + return(false); + + $json = @json_decode($data["body"]); + if (!is_object($json)) + return(false); + + return($json->nick == $user); + } + + /** + * @brief Authenticate the givven user and password + * + * @param array $aCommand The command array + */ + private function auth($aCommand) { + $a = get_app(); + + // check user authentication + if (sizeof($aCommand) != 4) { + $this->writeLog("[exAuth] invalid auth command, data missing"); + fwrite(STDOUT, pack("nn", 2, 0)); + return; + } + + // We now check if the password match + $sUser = str_replace(array("%20", "(a)"), array(" ", "@"), $aCommand[1]); + $this->writeDebugLog("[debug] doing auth for ".$sUser."@".$aCommand[2]); + + // Does the hostname match? So we try directly + if ($a->get_hostname() == $aCommand[2]) { + $sQuery = "SELECT `uid`, `password` FROM `user` WHERE `nickname`='".dbesc($sUser)."'"; + $this->writeDebugLog("[debug] using query ". $sQuery); + if ($oResult = q($sQuery)) { + $uid = $oResult[0]["uid"]; + $Error = ($oResult[0]["password"] != hash('whirlpool',$aCommand[3])); + } else { + $this->writeLog("[MySQL] invalid query: ". $sQuery); + $Error = true; + $uid = -1; + } + if ($Error) { + $oConfig = q("SELECT `v` FROM `pconfig` WHERE `uid` = %d AND `cat` = 'xmpp' AND `k`='password' LIMIT 1;", intval($uid)); + $this->writeLog("[exAuth] got password ".$oConfig[0]["v"]); + $Error = ($aCommand[3] != $oConfig[0]["v"]); + } + } else { + $Error = true; + } + + // If the hostnames doesn't match or there is some failure, we try to check remotely + if ($Error) { + $Error = !$this->check_credentials($aCommand[2], $aCommand[1], $aCommand[3], true); + } + + if ($Error) { + $this->writeLog("[exAuth] authentification failed for user ".$sUser."@". $aCommand[2]); + fwrite(STDOUT, pack("nn", 2, 0)); + } else { + $this->writeLog("[exAuth] authentificated user ".$sUser."@".$aCommand[2]); + fwrite(STDOUT, pack("nn", 2, 1)); + } + } + + /** + * @brief Check remote credentials via HTTP(S) + * + * @param string $host The hostname + * @param string $user Username + * @param string $password Password + * @param boolean $ssl Should the check be done via SSL? + * + * @return boolean Are the credentials okay? + */ + private function check_credentials($host, $user, $password, $ssl) { + $this->writeDebugLog("[debug] check credentials for user ".$user." on ".$host); + + $url = ($ssl ? "https":"http")."://".$host."/api/account/verify_credentials.json"; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_NOBODY, true); + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($ch, CURLOPT_USERPWD, $user.':'.$password); + + $header = curl_exec($ch); + $curl_info = @curl_getinfo($ch); + $http_code = $curl_info["http_code"]; + curl_close($ch); + + $this->writeDebugLog("[debug] got HTTP code ".$http_code); + + return ($http_code == 200); + } + + /** + * @brief write data to the logfile + * + * @param string $sMessage The logfile message + */ + private function writeLog($sMessage) { + if (is_resource($this->rLogFile)) + fwrite($this->rLogFile, date("r")." ".$sMessage."\n"); + } + + /** + * @brief write debug data to the logfile + * + * @param string $sMessage The logfile message + */ + private function writeDebugLog($sMessage) { + if ($this->bDebug) + $this->writeLog($sMessage); + } + + /** + * @brief destroy the class + */ + public function __destruct() { + // close the log file $this->writeLog("[exAuth] stop"); - if (is_resource($this->rLogFile)){ + if (is_resource($this->rLogFile)) fclose($this->rLogFile); - } } - - private function writeLog($sMessage) - { - if (is_resource($this->rLogFile)) { - fwrite($this->rLogFile, date("r") ." ". $sMessage ."\n"); - } - } - - private function writeDebugLog($sMessage) - { - if ($this->bDebug){ - $this->writeLog($sMessage); - } - } - } ?> - - diff --git a/include/autoloader.php b/include/autoloader.php new file mode 100644 index 0000000000..6caa082915 --- /dev/null +++ b/include/autoloader.php @@ -0,0 +1,69 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoloader/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoloader/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + $includeFiles = require __DIR__ . '/autoloader/autoload_files.php'; + foreach ($includeFiles as $fileIdentifier => $file) { + friendicaRequire($fileIdentifier, $file); + } + + + return $loader; + } +} + +function friendicaRequire($fileIdentifier, $file) +{ + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + require $file; + + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + } +} + + + +return FriendicaAutoloaderInit::getLoader(); diff --git a/include/autoloader/ClassLoader.php b/include/autoloader/ClassLoader.php new file mode 100644 index 0000000000..d916d802fe --- /dev/null +++ b/include/autoloader/ClassLoader.php @@ -0,0 +1,413 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE.composer + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + private $classMapAuthoritative = false; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative) { + return false; + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if ($file === null && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if ($file === null) { + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (is_file($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/include/autoloader/LICENSE.composer b/include/autoloader/LICENSE.composer new file mode 100644 index 0000000000..b365b1f5a7 --- /dev/null +++ b/include/autoloader/LICENSE.composer @@ -0,0 +1,19 @@ +Copyright (c) 2015 Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the Software), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, andor sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/include/autoloader/autoload_classmap.php b/include/autoloader/autoload_classmap.php new file mode 100644 index 0000000000..3efd09fc69 --- /dev/null +++ b/include/autoloader/autoload_classmap.php @@ -0,0 +1,9 @@ + $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', +); diff --git a/include/autoloader/autoload_namespaces.php b/include/autoloader/autoload_namespaces.php new file mode 100644 index 0000000000..315a349310 --- /dev/null +++ b/include/autoloader/autoload_namespaces.php @@ -0,0 +1,10 @@ + array($vendorDir . '/ezyang/htmlpurifier/library'), +); diff --git a/include/autoloader/autoload_psr4.php b/include/autoloader/autoload_psr4.php new file mode 100644 index 0000000000..d000ea28f6 --- /dev/null +++ b/include/autoloader/autoload_psr4.php @@ -0,0 +1,10 @@ + array($baseDir . '/include'), +); diff --git a/include/bb2diaspora.php b/include/bb2diaspora.php index a8b39f741a..f18a0f3c27 100644 --- a/include/bb2diaspora.php +++ b/include/bb2diaspora.php @@ -7,6 +7,27 @@ require_once("include/html2bbcode.php"); require_once("include/bbcode.php"); require_once("library/html-to-markdown/HTML_To_Markdown.php"); +/** + * @brief Callback function to replace a Diaspora style mention in a mention for Friendica + * + * @param array $match Matching values for the callback + * @return string Replaced mention + */ +function diaspora_mention2bb($match) { + if ($match[2] == '') { + return; + } + + $data = get_contact_details_by_addr($match[2]); + + $name = $match[1]; + + if ($name == '') { + $name = $data['name']; + } + + return '@[url='.$data['url'].']'.$name.'[/url]'; +} // we don't want to support a bbcode specific markdown interpreter // and the markdown library we have is pretty good, but provides HTML output. @@ -15,24 +36,28 @@ require_once("library/html-to-markdown/HTML_To_Markdown.php"); function diaspora2bb($s) { - $s = html_entity_decode($s,ENT_COMPAT,'UTF-8'); + $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8'); - // Remove CR to avoid problems with following code - $s = str_replace("\r","",$s); + // Handles single newlines + $s = str_replace("\r", '
', $s); - $s = str_replace("\n"," \n",$s); + $s = str_replace("\n", " \n", $s); + + // Replace lonely stars in lines not starting with it with literal stars + $s = preg_replace('/^([^\*]+)\*([^\*]*)$/im', '$1\*$2', $s); // The parser cannot handle paragraphs correctly - $s = str_replace(array("

", "

", '

'),array("
", "
", "
"),$s); + $s = str_replace(array('

', '

', '

'), array('
', '
', '
'), $s); // Escaping the hash tags - $s = preg_replace('/\#([^\s\#])/','#$1',$s); + $s = preg_replace('/\#([^\s\#])/', '#$1', $s); $s = Markdown($s); - $s = preg_replace('/\@\{(.+?)\; (.+?)\@(.+?)\}/','@[url=https://$3/u/$2]$1[/url]',$s); + $regexp = "/@\{(?:([^\}]+?); )?([^\} ]+)\}/"; + $s = preg_replace_callback($regexp, 'diaspora_mention2bb', $s); - $s = str_replace('#','#',$s); + $s = str_replace('#', '#', $s); $search = array(" \n", "\n "); $replace = array("\n", "\n"); @@ -41,23 +66,24 @@ function diaspora2bb($s) { $s = str_replace($search, $replace, $s); } while ($oldtext != $s); - $s = str_replace("\n\n", "
", $s); + $s = str_replace("\n\n", '
', $s); $s = html2bbcode($s); // protect the recycle symbol from turning into a tag, but without unescaping angles and naked ampersands - $s = str_replace('♲',html_entity_decode('♲',ENT_QUOTES,'UTF-8'),$s); + $s = str_replace('♲', html_entity_decode('♲', ENT_QUOTES, 'UTF-8'), $s); // Convert everything that looks like a link to a link - $s = preg_replace("/([^\]\=]|^)(https?\:\/\/)([a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url=$2$3]$2$3[/url]',$s); + $s = preg_replace('/([^\]=]|^)(https?\:\/\/)([a-zA-Z0-9:\/\-?&;.=_~#%$!+,@]+(?save_timestamp($stamp1, "parser"); @@ -128,6 +176,11 @@ function bb2diaspora($Text,$preserve_nl = false, $fordiaspora = true) { // the Diaspora signature verification and cause the item to disappear $Text = trim($Text); + if ($fordiaspora) { + $URLSearchString = "^\[\]"; + $Text = preg_replace_callback("/([@]\[(.*?)\])\(([$URLSearchString]*?)\)/ism", 'diaspora_mentions', $Text); + } + call_hooks('bb2diaspora',$Text); return $Text; @@ -140,8 +193,6 @@ function unescape_underscores_in_links($m) { function format_event_diaspora($ev) { - $a = get_app(); - if(! ((is_array($ev)) && count($ev))) return ''; @@ -156,7 +207,7 @@ function format_event_diaspora($ev) { $ev['start'] , $bd_format )) : day_translate(datetime_convert('UTC', 'UTC', $ev['start'] , $bd_format))) - . '](' . $a->get_baseurl() . '/localtime/?f=&time=' . urlencode(datetime_convert('UTC','UTC',$ev['start'])) . ")\n"; + . '](' . App::get_baseurl() . '/localtime/?f=&time=' . urlencode(datetime_convert('UTC','UTC',$ev['start'])) . ")\n"; if(! $ev['nofinish']) $o .= t('Finishes:') . ' ' . '[' @@ -164,7 +215,7 @@ function format_event_diaspora($ev) { $ev['finish'] , $bd_format )) : day_translate(datetime_convert('UTC', 'UTC', $ev['finish'] , $bd_format ))) - . '](' . $a->get_baseurl() . '/localtime/?f=&time=' . urlencode(datetime_convert('UTC','UTC',$ev['finish'])) . ")\n"; + . '](' . App::get_baseurl() . '/localtime/?f=&time=' . urlencode(datetime_convert('UTC','UTC',$ev['finish'])) . ")\n"; if(strlen($ev['location'])) $o .= t('Location:') . bb2diaspora($ev['location']) diff --git a/include/bbcode.php b/include/bbcode.php index 100c3b9306..74dde2fdf4 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -1,8 +1,12 @@ 0.9)) - $title2 = $url; - $text = sprintf('%s
', - $url, $title, $title2); - } elseif (($simplehtml != 4) AND ($simplehtml != 0)) - $text = sprintf('%s
', $url, $title); - else { - $text = sprintf('', $type); - - $bookmark = array(sprintf('[bookmark=%s]%s[/bookmark]', $url, $title), $url, $title); - if ($tryoembed) - $oembed = tryoembed($bookmark); - else - $oembed = $bookmark[0]; - - if (strstr(strtolower($oembed), "'; + if (!$height || strstr($height,'%')) { + $height = '200'; + } + $width = '100%'; + $s = App::get_baseurl() . '/oembed/' . base64url_encode($src); + return ''; } function oembed_bbcode2html($text){ - $stopoembed = get_config("system","no_oembed"); + $stopoembed = Config::get("system","no_oembed"); if ($stopoembed == true){ return preg_replace("/\[embed\](.+?)\[\/embed\]/is", "". t('Embedding disabled') ." : $1" ,$text); } @@ -237,13 +282,13 @@ function oe_build_xpath($attr, $value){ return "contains( normalize-space( @$attr ), ' $value ' ) or substring( normalize-space( @$attr ), 1, string-length( '$value' ) + 1 ) = '$value ' or substring( normalize-space( @$attr ), string-length( @$attr ) - string-length( '$value' ) ) = ' $value' or @$attr = '$value'"; } -function oe_get_inner_html( $node ) { - $innerHTML= ''; - $children = $node->childNodes; - foreach ($children as $child) { - $innerHTML .= $child->ownerDocument->saveXML( $child ); - } - return $innerHTML; +function oe_get_inner_html($node) { + $innerHTML= ''; + $children = $node->childNodes; + foreach ($children as $child) { + $innerHTML .= $child->ownerDocument->saveXML($child); + } + return $innerHTML; } /** @@ -252,15 +297,16 @@ function oe_get_inner_html( $node ) { */ function oembed_html2bbcode($text) { // start parser only if 'oembed' is in text - if (strpos($text, "oembed")){ + if (strpos($text, "oembed")) { // convert non ascii chars to html entities $html_text = mb_convert_encoding($text, 'HTML-ENTITIES', mb_detect_encoding($text)); // If it doesn't parse at all, just return the text. $dom = @DOMDocument::loadHTML($html_text); - if(! $dom) + if (! $dom) { return $text; + } $xpath = new DOMXPath($dom); $attr = "oembed"; diff --git a/include/onepoll.php b/include/onepoll.php index 6ff7eae422..5219d9f3bd 100644 --- a/include/onepoll.php +++ b/include/onepoll.php @@ -1,5 +1,7 @@ set_baseurl(get_config('system','url')); @@ -61,18 +59,10 @@ function onepoll_run(&$argv, &$argc){ return; } - $lockpath = get_lockpath(); - if ($lockpath != '') { - $pidfile = new pidfile($lockpath, 'onepoll'.$contact_id); - if ($pidfile->is_already_running()) { - logger("onepoll: Already running for contact ".$contact_id); - if ($pidfile->running_time() > 9*60) { - $pidfile->kill(); - logger("killed stale process"); - } - exit; - } - } + // Don't check this stuff if the function is called by the poller + if (App::callstack() != "poller_run") + if (App::is_already_running('onepoll'.$contact_id, '', 540)) + return; $d = datetime_convert(); @@ -104,14 +94,13 @@ function onepoll_run(&$argv, &$argc){ where `cid` = %d and updated > UTC_TIMESTAMP() - INTERVAL 1 DAY", intval($contact['id']) ); - if (count($r)) + if (dbm::is_result($r)) if (!$r[0]['total']) poco_load($contact['id'],$importer_uid,0,$contact['poco']); } - // To-Do: - // - Check why we don't poll the Diaspora feed at the moment (some guid problem in the items?) - // - Check whether this is possible with Redmatrix + /// @TODO Check why we don't poll the Diaspora feed at the moment (some guid problem in the items?) + /// @TODO Check whether this is possible with Redmatrix if ($contact["network"] == NETWORK_DIASPORA) { if (poco_do_update($contact["created"], $contact["last-item"], $contact["failure_update"], $contact["success_update"])) { $last_updated = poco_last_updated($contact["url"]); @@ -155,8 +144,9 @@ function onepoll_run(&$argv, &$argc){ $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `contact`.`uid` = `user`.`uid` WHERE `user`.`uid` = %d AND `contact`.`self` = 1 LIMIT 1", intval($importer_uid) ); - if(! count($r)) + if (! dbm::is_result($r)) { return; + } $importer = $r[0]; @@ -336,7 +326,9 @@ function onepoll_run(&$argv, &$argc){ if($contact['rel'] == CONTACT_IS_FOLLOWER || $contact['blocked'] || $contact['readonly']) return; - $xml = fetch_url($contact['poll']); + $cookiejar = tempnam(get_temppath(), 'cookiejar-onepoll-'); + $xml = fetch_url($contact['poll'], false, $redirects, 0, Null, $cookiejar); + unlink($cookiejar); } elseif($contact['network'] === NETWORK_MAIL || $contact['network'] === NETWORK_MAIL2) { @@ -403,7 +395,7 @@ function onepoll_run(&$argv, &$argc){ dbesc($datarray['uri']) ); - if(count($r)) { + if (dbm::is_result($r)) { logger("Mail: Seen before ".$msg_uid." for ".$mailconf[0]['user']." UID: ".$importer_uid." URI: ".$datarray['uri'],LOGGER_DEBUG); // Only delete when mails aren't automatically moved or deleted @@ -453,10 +445,10 @@ function onepoll_run(&$argv, &$argc){ $refs_arr[$x] = "'" . msgid2iri(str_replace(array('<','>',' '),array('','',''),dbesc($refs_arr[$x]))) . "'"; } $qstr = implode(',',$refs_arr); - $r = q("SELECT `uri` , `parent-uri` FROM `item` WHERE `uri` IN ( $qstr ) AND `uid` = %d LIMIT 1", + $r = q("SELECT `uri` , `parent-uri` FROM `item` USE INDEX (`uid_uri`) WHERE `uri` IN ($qstr) AND `uid` = %d LIMIT 1", intval($importer_uid) ); - if(count($r)) + if (dbm::is_result($r)) $datarray['parent-uri'] = $r[0]['parent-uri']; // Set the parent as the top-level item // $datarray['parent-uri'] = $r[0]['uri']; } @@ -485,10 +477,11 @@ function onepoll_run(&$argv, &$argc){ // If it seems to be a reply but a header couldn't be found take the last message with matching subject if(!x($datarray,'parent-uri') and $reply) { - $r = q("SELECT `uri` , `parent-uri` FROM `item` WHERE `title` = \"%s\" AND `uid` = %d ORDER BY `created` DESC LIMIT 1", + $r = q("SELECT `uri` , `parent-uri` FROM `item` WHERE `title` = \"%s\" AND `uid` = %d AND `network` = '%s' ORDER BY `created` DESC LIMIT 1", dbesc(protect_sprintf($datarray['title'])), - intval($importer_uid)); - if(count($r)) + intval($importer_uid), + dbesc(NETWORK_MAIL)); + if (dbm::is_result($r)) $datarray['parent-uri'] = $r[0]['parent-uri']; } @@ -507,7 +500,7 @@ function onepoll_run(&$argv, &$argc){ logger("Mail: Importing ".$msg_uid." for ".$mailconf[0]['user']); // some mailing lists have the original author as 'from' - add this sender info to msg body. - // todo: adding a gravatar for the original author would be cool + /// @TODO Adding a gravatar for the original author would be cool if(! stristr($meta->from,$contact['addr'])) { $from = imap_mime_header_decode($meta->from); diff --git a/include/ostatus.php b/include/ostatus.php index bcaef4f439..2c4b677a53 100644 --- a/include/ostatus.php +++ b/include/ostatus.php @@ -1,4 +1,8 @@ evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue; + $author["author-name"] = $xpath->evaluate('atom:author/atom:name/text()', $context)->item(0)->nodeValue; - if (!$r) - return; + $aliaslink = $author["author-link"]; - foreach ($r AS $contact) { - ostatus_follow_friends($contact["uid"], $contact["v"]); - set_pconfig($contact["uid"], "system", "ostatus_legacy_contact", ""); - } -} + $alternate = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0)->attributes; + if (is_object($alternate)) + foreach($alternate AS $attributes) + if ($attributes->name == "href") + $author["author-link"] = $attributes->textContent; -// This function doesn't work reliable by now. -function ostatus_follow_friends($uid, $url) { - $contact = probe_url($url); + $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `nurl` IN ('%s', '%s') AND `network` != '%s'", + intval($importer["uid"]), dbesc(normalise_link($author["author-link"])), + dbesc(normalise_link($aliaslink)), dbesc(NETWORK_STATUSNET)); + if ($r) { + $contact = $r[0]; + $author["contact-id"] = $r[0]["id"]; + } else + $author["contact-id"] = $contact["id"]; - if (!$contact) - return; - - $api = $contact["baseurl"]."/api/"; - - // Fetching friends - $data = z_fetch_url($api."statuses/friends.json?screen_name=".$contact["nick"]); - - if (!$data["success"]) - return; - - $friends = json_decode($data["body"]); - - foreach ($friends AS $friend) { - $url = $friend->statusnet_profile_url; - $r = q("SELECT `url` FROM `contact` WHERE `uid` = %d AND - (`nurl` = '%s' OR `alias` = '%s' OR `alias` = '%s') AND - `network` != '%s' LIMIT 1", - intval($uid), dbesc(normalise_link($url)), - dbesc(normalise_link($url)), dbesc($url), dbesc(NETWORK_STATUSNET)); - if (!$r) { - $data = probe_url($friend->statusnet_profile_url); - if ($data["network"] == NETWORK_OSTATUS) { - $result = new_contact($uid,$friend->statusnet_profile_url); - if ($result["success"]) - logger($friend->name." ".$url." - success", LOGGER_DEBUG); - else - logger($friend->name." ".$url." - failed", LOGGER_DEBUG); - } else - logger($friend->name." ".$url." - not OStatus", LOGGER_DEBUG); + $avatarlist = array(); + $avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context); + foreach($avatars AS $avatar) { + $href = ""; + $width = 0; + foreach($avatar->attributes AS $attributes) { + if ($attributes->name == "href") + $href = $attributes->textContent; + if ($attributes->name == "width") + $width = $attributes->textContent; + } + if (($width > 0) AND ($href != "")) + $avatarlist[$width] = $href; } - } -} - -function ostatus_fetchauthor($xpath, $context, $importer, &$contact, $onlyfetch) { - - $author = array(); - $author["author-link"] = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue; - $author["author-name"] = $xpath->evaluate('atom:author/atom:name/text()', $context)->item(0)->nodeValue; - - // Preserve the value - $authorlink = $author["author-link"]; - - $alternate = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0)->attributes; - if (is_object($alternate)) - foreach($alternate AS $attributes) - if ($attributes->name == "href") - $author["author-link"] = $attributes->textContent; - - $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `nurl` IN ('%s', '%s') AND `network` != '%s'", - intval($importer["uid"]), dbesc(normalise_link($author["author-link"])), - dbesc(normalise_link($authorlink)), dbesc(NETWORK_STATUSNET)); - if ($r) { - $contact = $r[0]; - $author["contact-id"] = $r[0]["id"]; - } else - $author["contact-id"] = $contact["id"]; - - $avatarlist = array(); - $avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context); - foreach($avatars AS $avatar) { - $href = ""; - $width = 0; - foreach($avatar->attributes AS $attributes) { - if ($attributes->name == "href") - $href = $attributes->textContent; - if ($attributes->name == "width") - $width = $attributes->textContent; + if (count($avatarlist) > 0) { + krsort($avatarlist); + $author["author-avatar"] = current($avatarlist); } - if (($width > 0) AND ($href != "")) - $avatarlist[$width] = $href; - } - if (count($avatarlist) > 0) { - krsort($avatarlist); - $author["author-avatar"] = current($avatarlist); - } - $displayname = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue; - if ($displayname != "") - $author["author-name"] = $displayname; + $displayname = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue; + if ($displayname != "") + $author["author-name"] = $displayname; - $author["owner-name"] = $author["author-name"]; - $author["owner-link"] = $author["author-link"]; - $author["owner-avatar"] = $author["author-avatar"]; + $author["owner-name"] = $author["author-name"]; + $author["owner-link"] = $author["author-link"]; + $author["owner-avatar"] = $author["author-avatar"]; - if ($r AND !$onlyfetch) { - // Update contact data - $update_contact = ($r[0]['name-date'] < datetime_convert('','','now -12 hours')); - if ($update_contact) { - logger("Update contact data for contact ".$contact["id"], LOGGER_DEBUG); + // Only update the contacts if it is an OStatus contact + if ($r AND !$onlyfetch AND ($contact["network"] == NETWORK_OSTATUS)) { + + // Update contact data + + // This query doesn't seem to work + // $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue; + // if ($value != "") + // $contact["notify"] = $value; + + // This query doesn't seem to work as well - I hate these queries + // $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue; + // if ($value != "") + // $contact["poll"] = $value; + + $value = $xpath->evaluate('atom:author/atom:uri/text()', $context)->item(0)->nodeValue; + if ($value != "") + $contact["alias"] = $value; $value = $xpath->evaluate('atom:author/poco:displayName/text()', $context)->item(0)->nodeValue; if ($value != "") @@ -148,1391 +123,2038 @@ function ostatus_fetchauthor($xpath, $context, $importer, &$contact, $onlyfetch) if ($value != "") $contact["location"] = $value; - q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `about` = '%s', `location` = '%s', `name-date` = '%s' WHERE `id` = %d AND `network` = '%s'", - dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["about"]), dbesc($contact["location"]), - dbesc(datetime_convert()), intval($contact["id"]), dbesc(NETWORK_OSTATUS)); + if (($contact["name"] != $r[0]["name"]) OR ($contact["nick"] != $r[0]["nick"]) OR ($contact["about"] != $r[0]["about"]) OR + ($contact["alias"] != $r[0]["alias"]) OR ($contact["location"] != $r[0]["location"])) { - poco_check($contact["url"], $contact["name"], $contact["network"], $author["author-avatar"], $contact["about"], $contact["location"], - "", "", "", datetime_convert(), 2, $contact["id"], $contact["uid"]); - } + logger("Update contact data for contact ".$contact["id"], LOGGER_DEBUG); - $update_photo = ($r[0]['avatar-date'] < datetime_convert('','','now -12 hours')); + q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `alias` = '%s', `about` = '%s', `location` = '%s', `name-date` = '%s' WHERE `id` = %d", + dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["alias"]), + dbesc($contact["about"]), dbesc($contact["location"]), + dbesc(datetime_convert()), intval($contact["id"])); - if ($update_photo AND isset($author["author-avatar"])) { - logger("Update profile picture for contact ".$contact["id"], LOGGER_DEBUG); - - $photos = import_profile_photo($author["author-avatar"], $importer["uid"], $contact["id"]); - - q("UPDATE `contact` SET `photo` = '%s', `thumb` = '%s', `micro` = '%s', `avatar-date` = '%s' WHERE `id` = %d AND `network` = '%s'", - dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), - dbesc(datetime_convert()), intval($contact["id"]), dbesc(NETWORK_OSTATUS)); - } - } - - return($author); -} - -function ostatus_salmon_author($xml, $importer) { - $a = get_app(); - - if ($xml == "") - return; - - $doc = new DOMDocument(); - @$doc->loadXML($xml); - - $xpath = new DomXPath($doc); - $xpath->registerNamespace('atom', "http://www.w3.org/2005/Atom"); - $xpath->registerNamespace('thr', "http://purl.org/syndication/thread/1.0"); - $xpath->registerNamespace('georss', "http://www.georss.org/georss"); - $xpath->registerNamespace('activity', "http://activitystrea.ms/spec/1.0/"); - $xpath->registerNamespace('media', "http://purl.org/syndication/atommedia"); - $xpath->registerNamespace('poco', "http://portablecontacts.net/spec/1.0"); - $xpath->registerNamespace('ostatus', "http://ostatus.org/schema/1.0"); - $xpath->registerNamespace('statusnet', "http://status.net/schema/api/1/"); - - $entries = $xpath->query('/atom:entry'); - - foreach ($entries AS $entry) { - // fetch the author - $author = ostatus_fetchauthor($xpath, $entry, $importer, $contact, true); - return $author; - } -} - -function ostatus_import($xml,$importer,&$contact, &$hub) { - - $a = get_app(); - - logger("Import OStatus message", LOGGER_DEBUG); - - if ($xml == "") - return; - - $doc = new DOMDocument(); - @$doc->loadXML($xml); - - $xpath = new DomXPath($doc); - $xpath->registerNamespace('atom', "http://www.w3.org/2005/Atom"); - $xpath->registerNamespace('thr', "http://purl.org/syndication/thread/1.0"); - $xpath->registerNamespace('georss', "http://www.georss.org/georss"); - $xpath->registerNamespace('activity', "http://activitystrea.ms/spec/1.0/"); - $xpath->registerNamespace('media', "http://purl.org/syndication/atommedia"); - $xpath->registerNamespace('poco', "http://portablecontacts.net/spec/1.0"); - $xpath->registerNamespace('ostatus', "http://ostatus.org/schema/1.0"); - $xpath->registerNamespace('statusnet', "http://status.net/schema/api/1/"); - - $gub = ""; - $hub_attributes = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0)->attributes; - if (is_object($hub_attributes)) - foreach($hub_attributes AS $hub_attribute) - if ($hub_attribute->name == "href") { - $hub = $hub_attribute->textContent; - logger("Found hub ".$hub, LOGGER_DEBUG); + poco_check($contact["url"], $contact["name"], $contact["network"], $author["author-avatar"], $contact["about"], $contact["location"], + "", "", "", datetime_convert(), 2, $contact["id"], $contact["uid"]); } - $header = array(); - $header["uid"] = $importer["uid"]; - $header["network"] = NETWORK_OSTATUS; - $header["type"] = "remote"; - $header["wall"] = 0; - $header["origin"] = 0; - $header["gravity"] = GRAVITY_PARENT; + if (isset($author["author-avatar"]) AND ($author["author-avatar"] != $r[0]['avatar'])) { + logger("Update profile picture for contact ".$contact["id"], LOGGER_DEBUG); - // it could either be a received post or a post we fetched by ourselves - // depending on that, the first node is different - $first_child = $doc->firstChild->tagName; + update_contact_avatar($author["author-avatar"], $importer["uid"], $contact["id"]); + } + + // Ensure that we are having this contact (with uid=0) + $cid = get_contact($author["author-link"], 0); + + if ($cid) { + // Update it with the current values + q("UPDATE `contact` SET `url` = '%s', `name` = '%s', `nick` = '%s', `alias` = '%s', + `about` = '%s', `location` = '%s', + `success_update` = '%s', `last-update` = '%s' + WHERE `id` = %d", + dbesc($author["author-link"]), dbesc($contact["name"]), dbesc($contact["nick"]), + dbesc($contact["alias"]), dbesc($contact["about"]), dbesc($contact["location"]), + dbesc(datetime_convert()), dbesc(datetime_convert()), intval($cid)); + + // Update the avatar + update_contact_avatar($author["author-avatar"], 0, $cid); + } + + $contact["generation"] = 2; + $contact["hide"] = false; // OStatus contacts are never hidden + $contact["photo"] = $author["author-avatar"]; + update_gcontact($contact); + } + + return($author); + } + + /** + * @brief Fetches author data from a given XML string + * + * @param string $xml The XML + * @param array $importer user record of the importing user + * + * @return array Array of author related entries for the item + */ + public static function salmon_author($xml, $importer) { + + if ($xml == "") + return; + + $doc = new DOMDocument(); + @$doc->loadXML($xml); + + $xpath = new DomXPath($doc); + $xpath->registerNamespace('atom', NAMESPACE_ATOM1); + $xpath->registerNamespace('thr', NAMESPACE_THREAD); + $xpath->registerNamespace('georss', NAMESPACE_GEORSS); + $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY); + $xpath->registerNamespace('media', NAMESPACE_MEDIA); + $xpath->registerNamespace('poco', NAMESPACE_POCO); + $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS); + $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET); - if ($first_child == "feed") - $entries = $xpath->query('/atom:feed/atom:entry'); - else $entries = $xpath->query('/atom:entry'); - $conversation = ""; - $conversationlist = array(); - $item_id = 0; - - // Reverse the order of the entries - $entrylist = array(); - - foreach ($entries AS $entry) - $entrylist[] = $entry; - - foreach (array_reverse($entrylist) AS $entry) { - - $mention = false; - - // fetch the author - if ($first_child == "feed") - $author = ostatus_fetchauthor($xpath, $doc->firstChild, $importer, $contact, false); - else - $author = ostatus_fetchauthor($xpath, $entry, $importer, $contact, false); - - $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $context)->item(0)->nodeValue; - if ($value != "") - $nickname = $value; - else - $nickname = $author["author-name"]; - - $item = array_merge($header, $author); - - // Now get the item - $item["uri"] = $xpath->query('atom:id/text()', $entry)->item(0)->nodeValue; - - $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", - intval($importer["uid"]), dbesc($item["uri"])); - if ($r) { - logger("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already existed under id ".$r[0]["id"], LOGGER_DEBUG); - continue; + foreach ($entries AS $entry) { + // fetch the author + $author = self::fetchauthor($xpath, $entry, $importer, $contact, true); + return $author; } + } - $item["body"] = add_page_info_to_body(html2bbcode($xpath->query('atom:content/text()', $entry)->item(0)->nodeValue)); - $item["object-type"] = $xpath->query('activity:object-type/text()', $entry)->item(0)->nodeValue; + /** + * @brief Imports an XML string containing OStatus elements + * + * @param string $xml The XML + * @param array $importer user record of the importing user + * @param $contact + * @param array $hub Called by reference, returns the fetched hub data + */ + public static function import($xml,$importer,&$contact, &$hub) { + /// @todo this function is too long. It has to be split in many parts - if (($item["object-type"] == ACTIVITY_OBJ_BOOKMARK) OR ($item["object-type"] == ACTIVITY_OBJ_EVENT)) { - $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue; - $item["body"] = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue; - } elseif ($item["object-type"] == ACTIVITY_OBJ_QUESTION) - $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue; + logger("Import OStatus message", LOGGER_DEBUG); - $item["object"] = $xml; - $item["verb"] = $xpath->query('activity:verb/text()', $entry)->item(0)->nodeValue; + if ($xml == "") + return; - // To-Do: - // Delete a message - if ($item["verb"] == "qvitter-delete-notice") { - // ignore "Delete" messages (by now) - logger("Ignore delete message ".print_r($item, true)); - continue; - } + //$tempfile = tempnam(get_temppath(), "import"); + //file_put_contents($tempfile, $xml); - if ($item["verb"] == ACTIVITY_JOIN) { - // ignore "Join" messages - logger("Ignore join message ".print_r($item, true)); - continue; - } + $doc = new DOMDocument(); + @$doc->loadXML($xml); - if ($item["verb"] == ACTIVITY_FOLLOW) { - new_follower($importer, $contact, $item, $nickname); - continue; - } + $xpath = new DomXPath($doc); + $xpath->registerNamespace('atom', NAMESPACE_ATOM1); + $xpath->registerNamespace('thr', NAMESPACE_THREAD); + $xpath->registerNamespace('georss', NAMESPACE_GEORSS); + $xpath->registerNamespace('activity', NAMESPACE_ACTIVITY); + $xpath->registerNamespace('media', NAMESPACE_MEDIA); + $xpath->registerNamespace('poco', NAMESPACE_POCO); + $xpath->registerNamespace('ostatus', NAMESPACE_OSTATUS); + $xpath->registerNamespace('statusnet', NAMESPACE_STATUSNET); - if ($item["verb"] == NAMESPACE_OSTATUS."/unfollow") { - lose_follower($importer, $contact, $item, $dummy); - continue; - } - - if ($item["verb"] == ACTIVITY_FAVORITE) { - $orig_uri = $xpath->query("activity:object/atom:id", $entry)->item(0)->nodeValue; - logger("Favorite ".$orig_uri." ".print_r($item, true)); - - $item["verb"] = ACTIVITY_LIKE; - $item["parent-uri"] = $orig_uri; - $item["gravity"] = GRAVITY_LIKE; - } - - if ($item["verb"] == NAMESPACE_OSTATUS."/unfavorite") { - // Ignore "Unfavorite" message - logger("Ignore unfavorite message ".print_r($item, true)); - continue; - } - - // http://activitystrea.ms/schema/1.0/rsvp-yes - if (!in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_LIKE, ACTIVITY_SHARE))) - logger("Unhandled verb ".$item["verb"]." ".print_r($item, true)); - - $item["created"] = $xpath->query('atom:published/text()', $entry)->item(0)->nodeValue; - $item["edited"] = $xpath->query('atom:updated/text()', $entry)->item(0)->nodeValue; - $conversation = $xpath->query('ostatus:conversation/text()', $entry)->item(0)->nodeValue; - - $related = ""; - - $inreplyto = $xpath->query('thr:in-reply-to', $entry); - if (is_object($inreplyto->item(0))) { - foreach($inreplyto->item(0)->attributes AS $attributes) { - if ($attributes->name == "ref") - $item["parent-uri"] = $attributes->textContent; - if ($attributes->name == "href") - $related = $attributes->textContent; - } - } - - $georsspoint = $xpath->query('georss:point', $entry); - if ($georsspoint) - $item["coord"] = $georsspoint->item(0)->nodeValue; - - // To-Do - // $item["location"] = - - $categories = $xpath->query('atom:category', $entry); - if ($categories) { - foreach ($categories AS $category) { - foreach($category->attributes AS $attributes) - if ($attributes->name == "term") { - $term = $attributes->textContent; - if(strlen($item["tag"])) - $item["tag"] .= ','; - $item["tag"] .= "#[url=".$a->get_baseurl()."/search?tag=".$term."]".$term."[/url]"; - } - } - } - - $self = ""; - $enclosure = ""; - - $links = $xpath->query('atom:link', $entry); - if ($links) { - $rel = ""; - $href = ""; - $type = ""; - $length = "0"; - $title = ""; - foreach ($links AS $link) { - foreach($link->attributes AS $attributes) { - if ($attributes->name == "href") - $href = $attributes->textContent; - if ($attributes->name == "rel") - $rel = $attributes->textContent; - if ($attributes->name == "type") - $type = $attributes->textContent; - if ($attributes->name == "length") - $length = $attributes->textContent; - if ($attributes->name == "title") - $title = $attributes->textContent; + $gub = ""; + $hub_attributes = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0)->attributes; + if (is_object($hub_attributes)) + foreach($hub_attributes AS $hub_attribute) + if ($hub_attribute->name == "href") { + $hub = $hub_attribute->textContent; + logger("Found hub ".$hub, LOGGER_DEBUG); } - if (($rel != "") AND ($href != "")) - switch($rel) { - case "alternate": - $item["plink"] = $href; - if (($item["object-type"] == ACTIVITY_OBJ_QUESTION) OR - ($item["object-type"] == ACTIVITY_OBJ_EVENT)) - $item["body"] .= add_page_info($href); - break; - case "ostatus:conversation": - $conversation = $href; - break; - case "enclosure": - $enclosure = $href; - if(strlen($item["attach"])) - $item["attach"] .= ','; - $item["attach"] .= '[attach]href="'.$href.'" length="'.$length.'" type="'.$type.'" title="'.$title.'"[/attach]'; - break; - case "related": - if ($item["object-type"] != ACTIVITY_OBJ_BOOKMARK) { - if (!isset($item["parent-uri"])) - $item["parent-uri"] = $href; + $header = array(); + $header["uid"] = $importer["uid"]; + $header["network"] = NETWORK_OSTATUS; + $header["type"] = "remote"; + $header["wall"] = 0; + $header["origin"] = 0; + $header["gravity"] = GRAVITY_PARENT; - if ($related == "") - $related = $href; - } else - $item["body"] .= add_page_info($href); - break; - case "self": - $self = $href; - break; - case "mentioned": - // Notification check - if ($importer["nurl"] == normalise_link($href)) - $mention = true; - break; - } + // it could either be a received post or a post we fetched by ourselves + // depending on that, the first node is different + $first_child = $doc->firstChild->tagName; + + if ($first_child == "feed") + $entries = $xpath->query('/atom:feed/atom:entry'); + else + $entries = $xpath->query('/atom:entry'); + + $conversation = ""; + $conversationlist = array(); + $item_id = 0; + + // Reverse the order of the entries + $entrylist = array(); + + foreach ($entries AS $entry) + $entrylist[] = $entry; + + foreach (array_reverse($entrylist) AS $entry) { + + $mention = false; + + // fetch the author + if ($first_child == "feed") + $author = self::fetchauthor($xpath, $doc->firstChild, $importer, $contact, false); + else + $author = self::fetchauthor($xpath, $entry, $importer, $contact, false); + + $value = $xpath->evaluate('atom:author/poco:preferredUsername/text()', $context)->item(0)->nodeValue; + if ($value != "") + $nickname = $value; + else + $nickname = $author["author-name"]; + + $item = array_merge($header, $author); + + // Now get the item + $item["uri"] = $xpath->query('atom:id/text()', $entry)->item(0)->nodeValue; + + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", + intval($importer["uid"]), dbesc($item["uri"])); + if ($r) { + logger("Item with uri ".$item["uri"]." for user ".$importer["uid"]." already existed under id ".$r[0]["id"], LOGGER_DEBUG); + continue; } - } - $local_id = ""; - $repeat_of = ""; + $item["body"] = add_page_info_to_body(html2bbcode($xpath->query('atom:content/text()', $entry)->item(0)->nodeValue)); + $item["object-type"] = $xpath->query('activity:object-type/text()', $entry)->item(0)->nodeValue; - $notice_info = $xpath->query('statusnet:notice_info', $entry); - if ($notice_info AND ($notice_info->length > 0)) { - foreach($notice_info->item(0)->attributes AS $attributes) { - if ($attributes->name == "source") - $item["app"] = strip_tags($attributes->textContent); - if ($attributes->name == "local_id") - $local_id = $attributes->textContent; - if ($attributes->name == "repeat_of") - $repeat_of = $attributes->textContent; + if (($item["object-type"] == ACTIVITY_OBJ_BOOKMARK) OR ($item["object-type"] == ACTIVITY_OBJ_EVENT)) { + $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue; + $item["body"] = $xpath->query('atom:summary/text()', $entry)->item(0)->nodeValue; + } elseif ($item["object-type"] == ACTIVITY_OBJ_QUESTION) + $item["title"] = $xpath->query('atom:title/text()', $entry)->item(0)->nodeValue; + + $item["object"] = $xml; + $item["verb"] = $xpath->query('activity:verb/text()', $entry)->item(0)->nodeValue; + + /// @TODO + /// Delete a message + if ($item["verb"] == "qvitter-delete-notice") { + // ignore "Delete" messages (by now) + logger("Ignore delete message ".print_r($item, true)); + continue; } - } - // Is it a repeated post? - if ($repeat_of != "") { - $activityobjects = $xpath->query('activity:object', $entry)->item(0); + if ($item["verb"] == ACTIVITY_JOIN) { + // ignore "Join" messages + logger("Ignore join message ".print_r($item, true)); + continue; + } - if (is_object($activityobjects)) { + if ($item["verb"] == ACTIVITY_FOLLOW) { + new_follower($importer, $contact, $item, $nickname); + continue; + } - $orig_uri = $xpath->query("activity:object/atom:id", $activityobjects)->item(0)->nodeValue; - if (!isset($orig_uri)) - $orig_uri = $xpath->query('atom:id/text()', $activityobjects)->item(0)->nodeValue; + if ($item["verb"] == NAMESPACE_OSTATUS."/unfollow") { + lose_follower($importer, $contact, $item, $dummy); + continue; + } - $orig_links = $xpath->query("activity:object/atom:link[@rel='alternate']", $activityobjects); - if ($orig_links AND ($orig_links->length > 0)) - foreach($orig_links->item(0)->attributes AS $attributes) + if ($item["verb"] == ACTIVITY_FAVORITE) { + $orig_uri = $xpath->query("activity:object/atom:id", $entry)->item(0)->nodeValue; + logger("Favorite ".$orig_uri." ".print_r($item, true)); + + $item["verb"] = ACTIVITY_LIKE; + $item["parent-uri"] = $orig_uri; + $item["gravity"] = GRAVITY_LIKE; + } + + if ($item["verb"] == NAMESPACE_OSTATUS."/unfavorite") { + // Ignore "Unfavorite" message + logger("Ignore unfavorite message ".print_r($item, true)); + continue; + } + + // http://activitystrea.ms/schema/1.0/rsvp-yes + if (!in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_LIKE, ACTIVITY_SHARE))) + logger("Unhandled verb ".$item["verb"]." ".print_r($item, true)); + + $item["created"] = $xpath->query('atom:published/text()', $entry)->item(0)->nodeValue; + $item["edited"] = $xpath->query('atom:updated/text()', $entry)->item(0)->nodeValue; + $conversation = $xpath->query('ostatus:conversation/text()', $entry)->item(0)->nodeValue; + + $related = ""; + + $inreplyto = $xpath->query('thr:in-reply-to', $entry); + if (is_object($inreplyto->item(0))) { + foreach($inreplyto->item(0)->attributes AS $attributes) { + if ($attributes->name == "ref") + $item["parent-uri"] = $attributes->textContent; + if ($attributes->name == "href") + $related = $attributes->textContent; + } + } + + $georsspoint = $xpath->query('georss:point', $entry); + if ($georsspoint) + $item["coord"] = $georsspoint->item(0)->nodeValue; + + $categories = $xpath->query('atom:category', $entry); + if ($categories) { + foreach ($categories AS $category) { + foreach($category->attributes AS $attributes) + if ($attributes->name == "term") { + $term = $attributes->textContent; + if(strlen($item["tag"])) + $item["tag"] .= ','; + $item["tag"] .= "#[url=".App::get_baseurl()."/search?tag=".$term."]".$term."[/url]"; + } + } + } + + $self = ""; + $enclosure = ""; + + $links = $xpath->query('atom:link', $entry); + if ($links) { + $rel = ""; + $href = ""; + $type = ""; + $length = "0"; + $title = ""; + foreach ($links AS $link) { + foreach($link->attributes AS $attributes) { if ($attributes->name == "href") - $orig_link = $attributes->textContent; + $href = $attributes->textContent; + if ($attributes->name == "rel") + $rel = $attributes->textContent; + if ($attributes->name == "type") + $type = $attributes->textContent; + if ($attributes->name == "length") + $length = $attributes->textContent; + if ($attributes->name == "title") + $title = $attributes->textContent; + } + if (($rel != "") AND ($href != "")) + switch($rel) { + case "alternate": + $item["plink"] = $href; + if (($item["object-type"] == ACTIVITY_OBJ_QUESTION) OR + ($item["object-type"] == ACTIVITY_OBJ_EVENT)) + $item["body"] .= add_page_info($href); + break; + case "ostatus:conversation": + $conversation = $href; + break; + case "enclosure": + $enclosure = $href; + if(strlen($item["attach"])) + $item["attach"] .= ','; - if (!isset($orig_link)) - $orig_link = $xpath->query("atom:link[@rel='alternate']", $activityobjects)->item(0)->nodeValue; + $item["attach"] .= '[attach]href="'.$href.'" length="'.$length.'" type="'.$type.'" title="'.$title.'"[/attach]'; + break; + case "related": + if ($item["object-type"] != ACTIVITY_OBJ_BOOKMARK) { + if (!isset($item["parent-uri"])) + $item["parent-uri"] = $href; - if (!isset($orig_link)) - $orig_link = ostatus_convert_href($orig_uri); + if ($related == "") + $related = $href; + } else + $item["body"] .= add_page_info($href); + break; + case "self": + $self = $href; + break; + case "mentioned": + // Notification check + if ($importer["nurl"] == normalise_link($href)) + $mention = true; + break; + } + } + } - $orig_body = $xpath->query('activity:object/atom:content/text()', $activityobjects)->item(0)->nodeValue; - if (!isset($orig_body)) - $orig_body = $xpath->query('atom:content/text()', $activityobjects)->item(0)->nodeValue; + $local_id = ""; + $repeat_of = ""; - $orig_created = $xpath->query('atom:published/text()', $activityobjects)->item(0)->nodeValue; + $notice_info = $xpath->query('statusnet:notice_info', $entry); + if ($notice_info AND ($notice_info->length > 0)) { + foreach($notice_info->item(0)->attributes AS $attributes) { + if ($attributes->name == "source") + $item["app"] = strip_tags($attributes->textContent); + if ($attributes->name == "local_id") + $local_id = $attributes->textContent; + if ($attributes->name == "repeat_of") + $repeat_of = $attributes->textContent; + } + } - $orig_contact = $contact; - $orig_author = ostatus_fetchauthor($xpath, $activityobjects, $importer, $orig_contact, false); + // Is it a repeated post? + if (($repeat_of != "") OR ($item["verb"] == ACTIVITY_SHARE)) { + $activityobjects = $xpath->query('activity:object', $entry)->item(0); + + if (is_object($activityobjects)) { + + $orig_uri = $xpath->query("activity:object/atom:id", $activityobjects)->item(0)->nodeValue; + if (!isset($orig_uri)) + $orig_uri = $xpath->query('atom:id/text()', $activityobjects)->item(0)->nodeValue; + + $orig_links = $xpath->query("activity:object/atom:link[@rel='alternate']", $activityobjects); + if ($orig_links AND ($orig_links->length > 0)) + foreach($orig_links->item(0)->attributes AS $attributes) + if ($attributes->name == "href") + $orig_link = $attributes->textContent; + + if (!isset($orig_link)) + $orig_link = $xpath->query("atom:link[@rel='alternate']", $activityobjects)->item(0)->nodeValue; + + if (!isset($orig_link)) + $orig_link = self::convert_href($orig_uri); + + $orig_body = $xpath->query('activity:object/atom:content/text()', $activityobjects)->item(0)->nodeValue; + if (!isset($orig_body)) + $orig_body = $xpath->query('atom:content/text()', $activityobjects)->item(0)->nodeValue; + + $orig_created = $xpath->query('atom:published/text()', $activityobjects)->item(0)->nodeValue; + $orig_edited = $xpath->query('atom:updated/text()', $activityobjects)->item(0)->nodeValue; + + $orig_contact = $contact; + $orig_author = self::fetchauthor($xpath, $activityobjects, $importer, $orig_contact, false); - //if (!intval(get_config('system','wall-to-wall_share'))) { - // $prefix = share_header($orig_author['author-name'], $orig_author['author-link'], $orig_author['author-avatar'], "", $orig_created, $orig_link); - // $item["body"] = $prefix.add_page_info_to_body(html2bbcode($orig_body))."[/share]"; - //} else { $item["author-name"] = $orig_author["author-name"]; $item["author-link"] = $orig_author["author-link"]; $item["author-avatar"] = $orig_author["author-avatar"]; $item["body"] = add_page_info_to_body(html2bbcode($orig_body)); $item["created"] = $orig_created; + $item["edited"] = $orig_edited; $item["uri"] = $orig_uri; $item["plink"] = $orig_link; - //} - $item["verb"] = $xpath->query('activity:verb/text()', $activityobjects)->item(0)->nodeValue; + $item["verb"] = $xpath->query('activity:verb/text()', $activityobjects)->item(0)->nodeValue; - $item["object-type"] = $xpath->query('activity:object/activity:object-type/text()', $activityobjects)->item(0)->nodeValue; - if (!isset($item["object-type"])) - $item["object-type"] = $xpath->query('activity:object-type/text()', $activityobjects)->item(0)->nodeValue; - } - } - - //if ($enclosure != "") - // $item["body"] .= add_page_info($enclosure); - - if (isset($item["parent-uri"])) { - $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", - intval($importer["uid"]), dbesc($item["parent-uri"])); - - if (!$r AND ($related != "")) { - $reply_path = str_replace("/notice/", "/api/statuses/show/", $related).".atom"; - - if ($reply_path != $related) { - logger("Fetching related items for user ".$importer["uid"]." from ".$reply_path, LOGGER_DEBUG); - $reply_xml = fetch_url($reply_path); - - $reply_contact = $contact; - ostatus_import($reply_xml,$importer,$reply_contact, $reply_hub); - - // After the import try to fetch the parent item again - $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", - intval($importer["uid"]), dbesc($item["parent-uri"])); + $item["object-type"] = $xpath->query('activity:object/activity:object-type/text()', $activityobjects)->item(0)->nodeValue; + if (!isset($item["object-type"])) + $item["object-type"] = $xpath->query('activity:object-type/text()', $activityobjects)->item(0)->nodeValue; } } - if ($r) { - $item["type"] = 'remote-comment'; - $item["gravity"] = GRAVITY_COMMENT; + + //if ($enclosure != "") + // $item["body"] .= add_page_info($enclosure); + + if (isset($item["parent-uri"])) { + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", + intval($importer["uid"]), dbesc($item["parent-uri"])); + + // Only fetch missing stuff if it is a comment or reshare. + if (in_array($item["verb"], array(ACTIVITY_POST, ACTIVITY_SHARE)) AND + !dbm::is_result($r) AND ($related != "")) { + $reply_path = str_replace("/notice/", "/api/statuses/show/", $related).".atom"; + + if ($reply_path != $related) { + logger("Fetching related items for user ".$importer["uid"]." from ".$reply_path, LOGGER_DEBUG); + $reply_xml = fetch_url($reply_path); + + $reply_contact = $contact; + self::import($reply_xml,$importer,$reply_contact, $reply_hub); + + // After the import try to fetch the parent item again + $r = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", + intval($importer["uid"]), dbesc($item["parent-uri"])); + } + } + if ($r) { + $item["type"] = 'remote-comment'; + $item["gravity"] = GRAVITY_COMMENT; + } + } else + $item["parent-uri"] = $item["uri"]; + + $item_id = self::completion($conversation, $importer["uid"], $item, $self); + + if (!$item_id) { + logger("Error storing item", LOGGER_DEBUG); + continue; } - } else - $item["parent-uri"] = $item["uri"]; - $item_id = ostatus_completion($conversation, $importer["uid"], $item); - - if (!$item_id) { - logger("Error storing item", LOGGER_DEBUG); - continue; - } - - logger("Item was stored with id ".$item_id, LOGGER_DEBUG); - $item["id"] = $item_id; - - if ($mention) { - $u = q("SELECT `notify-flags`, `language`, `username`, `email` FROM user WHERE uid = %d LIMIT 1", intval($item['uid'])); - $r = q("SELECT `parent` FROM `item` WHERE `id` = %d", intval($item_id)); - - notification(array( - 'type' => NOTIFY_TAGSELF, - 'notify_flags' => $u[0]["notify-flags"], - 'language' => $u[0]["language"], - 'to_name' => $u[0]["username"], - 'to_email' => $u[0]["email"], - 'uid' => $item["uid"], - 'item' => $item, - 'link' => $a->get_baseurl().'/display/'.urlencode(get_item_guid($item_id)), - 'source_name' => $item["author-name"], - 'source_link' => $item["author-link"], - 'source_photo' => $item["author-avatar"], - 'verb' => ACTIVITY_TAG, - 'otype' => 'item', - 'parent' => $r[0]["parent"] - )); + logger("Item was stored with id ".$item_id, LOGGER_DEBUG); } } -} -function ostatus_convert_href($href) { - $elements = explode(":",$href); + /** + * @brief Create an url out of an uri + * + * @param string $href URI in the format "parameter1:parameter1:..." + * + * @return string URL in the format http(s)://.... + */ + public static function convert_href($href) { + $elements = explode(":",$href); + + if ((count($elements) <= 2) OR ($elements[0] != "tag")) + return $href; + + $server = explode(",", $elements[1]); + $conversation = explode("=", $elements[2]); + + if ((count($elements) == 4) AND ($elements[2] == "post")) + return "http://".$server[0]."/notice/".$elements[3]; + + if ((count($conversation) != 2) OR ($conversation[1] =="")) + return $href; + + if ($elements[3] == "objectType=thread") + return "http://".$server[0]."/conversation/".$conversation[1]; + else + return "http://".$server[0]."/notice/".$conversation[1]; - if ((count($elements) <= 2) OR ($elements[0] != "tag")) return $href; - - $server = explode(",", $elements[1]); - $conversation = explode("=", $elements[2]); - - if ((count($elements) == 4) AND ($elements[2] == "post")) - return "http://".$server[0]."/notice/".$elements[3]; - - if ((count($conversation) != 2) OR ($conversation[1] =="")) - return $href; - - if ($elements[3] == "objectType=thread") - return "http://".$server[0]."/conversation/".$conversation[1]; - else - return "http://".$server[0]."/notice/".$conversation[1]; - - return $href; -} - -function check_conversations($mentions = false, $override = false) { - $last = get_config('system','ostatus_last_poll'); - - $poll_interval = intval(get_config('system','ostatus_poll_interval')); - if(! $poll_interval) - $poll_interval = OSTATUS_DEFAULT_POLL_INTERVAL; - - // Don't poll if the interval is set negative - if (($poll_interval < 0) AND !$override) - return; - - if (!$mentions) { - $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe')); - if (!$poll_timeframe) - $poll_timeframe = OSTATUS_DEFAULT_POLL_TIMEFRAME; - } else { - $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe')); - if (!$poll_timeframe) - $poll_timeframe = OSTATUS_DEFAULT_POLL_TIMEFRAME_MENTIONS; } + /** + * @brief Checks if there are entries in conversations that aren't present on our side + * + * @param bool $mentions Fetch conversations where we are mentioned + * @param bool $override Override the interval setting + */ + public static function check_conversations($mentions = false, $override = false) { + $last = get_config('system','ostatus_last_poll'); - if ($last AND !$override) { - $next = $last + ($poll_interval * 60); - if ($next > time()) { - logger('poll interval not reached'); + $poll_interval = intval(get_config('system','ostatus_poll_interval')); + if (!$poll_interval) { + $poll_interval = self::OSTATUS_DEFAULT_POLL_INTERVAL; + } + + // Don't poll if the interval is set negative + if (($poll_interval < 0) AND !$override) { return; } - } - logger('cron_start'); - - $start = date("Y-m-d H:i:s", time() - ($poll_timeframe * 60)); - - if ($mentions) - $conversations = q("SELECT `term`.`oid`, `term`.`url`, `term`.`uid` FROM `term` - STRAIGHT_JOIN `thread` ON `thread`.`iid` = `term`.`oid` AND `thread`.`uid` = `term`.`uid` - WHERE `term`.`type` = 7 AND `term`.`term` > '%s' AND `thread`.`mention` - GROUP BY `term`.`url`, `term`.`uid` ORDER BY `term`.`term` DESC", dbesc($start)); - else - $conversations = q("SELECT `oid`, `url`, `uid` FROM `term` - WHERE `type` = 7 AND `term` > '%s' - GROUP BY `url`, `uid` ORDER BY `term` DESC", dbesc($start)); - - foreach ($conversations AS $conversation) { - ostatus_completion($conversation['url'], $conversation['uid']); - } - - logger('cron_end'); - - set_config('system','ostatus_last_poll', time()); -} - -function ostatus_completion($conversation_url, $uid, $item = array()) { - - $a = get_app(); - - $item_stored = -1; - - $conversation_url = ostatus_convert_href($conversation_url); - - // If the thread shouldn't be completed then store the item and go away - if ((intval(get_config('system','ostatus_poll_interval')) == -2) AND (count($item) > 0)) { - //$arr["app"] .= " (OStatus-NoCompletion)"; - $item_stored = item_store($item, true); - return($item_stored); - } - - // Get the parent - $parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN - (SELECT `parent` FROM `item` WHERE `id` IN - (SELECT `oid` FROM `term` WHERE `uid` = %d AND `otype` = %d AND `type` = %d AND `url` = '%s'))", - intval($uid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), dbesc($conversation_url)); - - if ($parents) - $parent = $parents[0]; - elseif (count($item) > 0) { - $parent = $item; - $parent["type"] = "remote"; - $parent["verb"] = ACTIVITY_POST; - $parent["visible"] = 1; - } else { - // Preset the parent - $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid`=%d", $uid); - if (!$r) - return(-2); - - $parent = array(); - $parent["id"] = 0; - $parent["parent"] = 0; - $parent["uri"] = ""; - $parent["contact-id"] = $r[0]["id"]; - $parent["type"] = "remote"; - $parent["verb"] = ACTIVITY_POST; - $parent["visible"] = 1; - } - - $conv = str_replace("/conversation/", "/api/statusnet/conversation/", $conversation_url).".as"; - $pageno = 1; - $items = array(); - - logger('fetching conversation url '.$conv.' for user '.$uid); - - do { - $conv_arr = z_fetch_url($conv."?page=".$pageno); - - // If it is a non-ssl site and there is an error, then try ssl or vice versa - if (!$conv_arr["success"] AND (substr($conv, 0, 7) == "http://")) { - $conv = str_replace("http://", "https://", $conv); - $conv_as = fetch_url($conv."?page=".$pageno); - } elseif (!$conv_arr["success"] AND (substr($conv, 0, 8) == "https://")) { - $conv = str_replace("https://", "http://", $conv); - $conv_as = fetch_url($conv."?page=".$pageno); - } else - $conv_as = $conv_arr["body"]; - - $conv_as = str_replace(',"statusnet:notice_info":', ',"statusnet_notice_info":', $conv_as); - $conv_as = json_decode($conv_as); - - $no_of_items = sizeof($items); - - if (@is_array($conv_as->items)) - foreach ($conv_as->items AS $single_item) - $items[$single_item->id] = $single_item; - - if ($no_of_items == sizeof($items)) - break; - - $pageno++; - - } while (true); - - logger('fetching conversation done. Found '.count($items).' items'); - - if (!sizeof($items)) { - if (count($item) > 0) { - //$arr["app"] .= " (OStatus-NoConvFetched)"; - $item_stored = item_store($item, true); - - if ($item_stored) { - logger("Conversation ".$conversation_url." couldn't be fetched. Item uri ".$item["uri"]." stored: ".$item_stored, LOGGER_DEBUG); - ostatus_store_conversation($item_id, $conversation_url); + if (!$mentions) { + $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe')); + if (!$poll_timeframe) { + $poll_timeframe = self::OSTATUS_DEFAULT_POLL_TIMEFRAME; } - - return($item_stored); - } else - return(-3); - } - - $items = array_reverse($items); - - $r = q("SELECT `nurl` FROM `contact` WHERE `uid` = %d AND `self`", intval($uid)); - $importer = $r[0]; - - foreach ($items as $single_conv) { - - // Test - remove before flight - //$tempfile = tempnam(get_temppath(), "conversation"); - //file_put_contents($tempfile, json_encode($single_conv)); - - $mention = false; - - if (isset($single_conv->object->id)) - $single_conv->id = $single_conv->object->id; - - $plink = ostatus_convert_href($single_conv->id); - if (isset($single_conv->object->url)) - $plink = ostatus_convert_href($single_conv->object->url); - - if (@!$single_conv->id) - continue; - - logger("Got id ".$single_conv->id, LOGGER_DEBUG); - - if ($first_id == "") { - $first_id = $single_conv->id; - - // The first post of the conversation isn't our first post. There are three options: - // 1. Our conversation hasn't the "real" thread starter - // 2. This first post is a post inside our thread - // 3. This first post is a post inside another thread - if (($first_id != $parent["uri"]) AND ($parent["uri"] != "")) { - $new_parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN - (SELECT `parent` FROM `item` - WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s')) LIMIT 1", - intval($uid), dbesc($first_id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); - if ($new_parents) { - if ($new_parents[0]["parent"] == $parent["parent"]) { - // Option 2: This post is already present inside our thread - but not as thread starter - logger("Option 2: uri present in our thread: ".$first_id, LOGGER_DEBUG); - $first_id = $parent["uri"]; - } else { - // Option 3: Not so good. We have mixed parents. We have to see how to clean this up. - // For now just take the new parent. - $parent = $new_parents[0]; - $first_id = $parent["uri"]; - logger("Option 3: mixed parents for uri ".$first_id, LOGGER_DEBUG); - } - } else { - // Option 1: We hadn't got the real thread starter - // We have to clean up our existing messages. - $parent["id"] = 0; - $parent["uri"] = $first_id; - logger("Option 1: we have a new parent: ".$first_id, LOGGER_DEBUG); - } - } elseif ($parent["uri"] == "") { - $parent["id"] = 0; - $parent["uri"] = $first_id; - } - } - - $parent_uri = $parent["uri"]; - - // "context" only seems to exist on older servers - if (isset($single_conv->context->inReplyTo->id)) { - $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", - intval($uid), dbesc($single_conv->context->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); - if ($parent_exists) - $parent_uri = $single_conv->context->inReplyTo->id; - } - - // This is the current way - if (isset($single_conv->object->inReplyTo->id)) { - $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", - intval($uid), dbesc($single_conv->object->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); - if ($parent_exists) - $parent_uri = $single_conv->object->inReplyTo->id; - } - - $message_exists = q("SELECT `id`, `parent`, `uri` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", - intval($uid), dbesc($single_conv->id), - dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); - if ($message_exists) { - logger("Message ".$single_conv->id." already existed on the system", LOGGER_DEBUG); - - if ($parent["id"] != 0) { - $existing_message = $message_exists[0]; - - // We improved the way we fetch OStatus messages, this shouldn't happen very often now - // To-Do: we have to change the shadow copies as well. This way here is really ugly. - if ($existing_message["parent"] != $parent["id"]) { - logger('updating id '.$existing_message["id"].' with parent '.$existing_message["parent"].' to parent '.$parent["id"].' uri '.$parent["uri"].' thread '.$parent_uri, LOGGER_DEBUG); - - // Update the parent id of the selected item - $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `id` = %d", - intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["id"])); - - // Update the parent uri in the thread - but only if it points to itself - $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE `id` = %d AND `uri` = `thr-parent`", - dbesc($parent_uri), intval($existing_message["id"])); - - // try to change all items of the same parent - $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `parent` = %d", - intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["parent"])); - - // Update the parent uri in the thread - but only if it points to itself - $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE (`parent` = %d) AND (`uri` = `thr-parent`)", - dbesc($parent["uri"]), intval($existing_message["parent"])); - - // Now delete the thread - delete_thread($existing_message["parent"]); - } - } - - // The item we are having on the system is the one that we wanted to store via the item array - if (isset($item["uri"]) AND ($item["uri"] == $existing_message["uri"])) { - $item = array(); - $item_stored = 0; - } - - continue; - } - - if (is_array($single_conv->to)) - foreach($single_conv->to AS $to) - if ($importer["nurl"] == normalise_link($to->id)) - $mention = true; - - $actor = $single_conv->actor->id; - if (isset($single_conv->actor->url)) - $actor = $single_conv->actor->url; - - $contact = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s' AND `network` != '%s'", - $uid, normalise_link($actor), NETWORK_STATUSNET); - - if (count($contact)) { - logger("Found contact for url ".$actor, LOGGER_DEBUG); - $contact_id = $contact[0]["id"]; } else { - logger("No contact found for url ".$actor, LOGGER_DEBUG); + $poll_timeframe = intval(get_config('system','ostatus_poll_timeframe')); + if (!$poll_timeframe) { + $poll_timeframe = self::OSTATUS_DEFAULT_POLL_TIMEFRAME_MENTIONS; + } + } + + + if ($last AND !$override) { + $next = $last + ($poll_interval * 60); + if ($next > time()) { + logger('poll interval not reached'); + return; + } + } + + logger('cron_start'); + + $start = date("Y-m-d H:i:s", time() - ($poll_timeframe * 60)); + + if ($mentions) { + $conversations = q("SELECT `term`.`oid`, `term`.`url`, `term`.`uid` FROM `term` + STRAIGHT_JOIN `thread` ON `thread`.`iid` = `term`.`oid` AND `thread`.`uid` = `term`.`uid` + WHERE `term`.`type` = 7 AND `term`.`term` > '%s' AND `thread`.`mention` + GROUP BY `term`.`url`, `term`.`uid` ORDER BY `term`.`term` DESC", dbesc($start)); + } else { + $conversations = q("SELECT `oid`, `url`, `uid` FROM `term` + WHERE `type` = 7 AND `term` > '%s' + GROUP BY `url`, `uid` ORDER BY `term` DESC", dbesc($start)); + } + + foreach ($conversations AS $conversation) { + self::completion($conversation['url'], $conversation['uid']); + } + + logger('cron_end'); + + set_config('system','ostatus_last_poll', time()); + } + + /** + * @brief Updates the gcontact table with actor data from the conversation + * + * @param object $actor The actor object that contains the contact data + */ + private function conv_fetch_actor($actor) { + + // We set the generation to "3" since the data here is not as reliable as the data we get on other occasions + $contact = array("network" => NETWORK_OSTATUS, "generation" => 3); + + if (isset($actor->url)) + $contact["url"] = $actor->url; + + if (isset($actor->displayName)) + $contact["name"] = $actor->displayName; + + if (isset($actor->portablecontacts_net->displayName)) + $contact["name"] = $actor->portablecontacts_net->displayName; + + if (isset($actor->portablecontacts_net->preferredUsername)) + $contact["nick"] = $actor->portablecontacts_net->preferredUsername; + + if (isset($actor->id)) + $contact["alias"] = $actor->id; + + if (isset($actor->summary)) + $contact["about"] = $actor->summary; + + if (isset($actor->portablecontacts_net->note)) + $contact["about"] = $actor->portablecontacts_net->note; + + if (isset($actor->portablecontacts_net->addresses->formatted)) + $contact["location"] = $actor->portablecontacts_net->addresses->formatted; + + + if (isset($actor->image->url)) + $contact["photo"] = $actor->image->url; + + if (isset($actor->image->width)) + $avatarwidth = $actor->image->width; + + if (is_array($actor->status_net->avatarLinks)) + foreach ($actor->status_net->avatarLinks AS $avatar) { + if ($avatarsize < $avatar->width) { + $contact["photo"] = $avatar->url; + $avatarsize = $avatar->width; + } + } + + $contact["hide"] = false; // OStatus contacts are never hidden + update_gcontact($contact); + } + + /** + * @brief Fetches the conversation url for a given item link or conversation id + * + * @param string $self The link to the posting + * @param string $conversation_id The conversation id + * + * @return string The conversation url + */ + private function fetch_conversation($self, $conversation_id = "") { + + if ($conversation_id != "") { + $elements = explode(":", $conversation_id); + + if ((count($elements) <= 2) OR ($elements[0] != "tag")) + return $conversation_id; + } + + if ($self == "") + return ""; + + $json = str_replace(".atom", ".json", $self); + + $raw = fetch_url($json); + if ($raw == "") + return ""; + + $data = json_decode($raw); + if (!is_object($data)) + return ""; + + $conversation_id = $data->statusnet_conversation_id; + + $pos = strpos($self, "/api/statuses/show/"); + $base_url = substr($self, 0, $pos); + + return $base_url."/conversation/".$conversation_id; + } + + /** + * @brief Fetches actor details of a given actor and user id + * + * @param string $actor The actor url + * @param int $uid The user id + * @param int $contact_id The default contact-id + * + * @return array Array with actor details + */ + private function get_actor_details($actor, $uid, $contact_id) { + + $details = array(); + + $contact = q("SELECT `id`, `rel`, `network` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s' AND `network` != '%s'", + $uid, normalise_link($actor), NETWORK_STATUSNET); + + if (!$contact) + $contact = q("SELECT `id`, `rel`, `network` FROM `contact` WHERE `uid` = %d AND `alias` IN ('%s', '%s') AND `network` != '%s'", + $uid, $actor, normalise_link($actor), NETWORK_STATUSNET); + + if ($contact) { + logger("Found contact for url ".$actor, LOGGER_DEBUG); + $details["contact_id"] = $contact[0]["id"]; + $details["network"] = $contact[0]["network"]; + + $details["not_following"] = !in_array($contact[0]["rel"], array(CONTACT_IS_SHARING, CONTACT_IS_FRIEND)); + } else { + logger("No contact found for user ".$uid." and url ".$actor, LOGGER_DEBUG); // Adding a global contact - // To-Do: Use this data for the post - $global_contact_id = get_contact($actor, 0); + /// @TODO Use this data for the post + $details["global_contact_id"] = get_contact($actor, 0); logger("Global contact ".$global_contact_id." found for url ".$actor, LOGGER_DEBUG); - $contact_id = $parent["contact-id"]; + $details["contact_id"] = $contact_id; + $details["network"] = NETWORK_OSTATUS; + + $details["not_following"] = true; } - $arr = array(); - $arr["network"] = NETWORK_OSTATUS; - $arr["uri"] = $single_conv->id; - $arr["plink"] = $plink; - $arr["uid"] = $uid; - $arr["contact-id"] = $contact_id; - $arr["parent-uri"] = $parent_uri; - $arr["created"] = $single_conv->published; - $arr["edited"] = $single_conv->published; - $arr["owner-name"] = $single_conv->actor->displayName; - if ($arr["owner-name"] == '') - $arr["owner-name"] = $single_conv->actor->contact->displayName; - if ($arr["owner-name"] == '') - $arr["owner-name"] = $single_conv->actor->portablecontacts_net->displayName; + return $details; + } - $arr["owner-link"] = $actor; - $arr["owner-avatar"] = $single_conv->actor->image->url; - $arr["author-name"] = $arr["owner-name"]; - $arr["author-link"] = $actor; - $arr["author-avatar"] = $single_conv->actor->image->url; - $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->content)); + /** + * @brief Stores an item and completes the thread + * + * @param string $conversation_url The URI of the conversation + * @param integer $uid The user id + * @param array $item Data of the item that is to be posted + * + * @return integer The item id of the posted item array + */ + private function completion($conversation_url, $uid, $item = array(), $self = "") { - if (isset($single_conv->status_net->notice_info->source)) - $arr["app"] = strip_tags($single_conv->status_net->notice_info->source); - elseif (isset($single_conv->statusnet->notice_info->source)) - $arr["app"] = strip_tags($single_conv->statusnet->notice_info->source); - elseif (isset($single_conv->statusnet_notice_info->source)) - $arr["app"] = strip_tags($single_conv->statusnet_notice_info->source); - elseif (isset($single_conv->provider->displayName)) - $arr["app"] = $single_conv->provider->displayName; - else - $arr["app"] = "OStatus"; + /// @todo This function is totally ugly and has to be rewritten totally - //$arr["app"] .= " (Conversation)"; + $item_stored = -1; - $arr["object"] = json_encode($single_conv); - $arr["verb"] = $parent["verb"]; - $arr["visible"] = $parent["visible"]; - $arr["location"] = $single_conv->location->displayName; - $arr["coord"] = trim($single_conv->location->lat." ".$single_conv->location->lon); + $conversation_url = self::fetch_conversation($self, $conversation_url); - // Is it a reshared item? - if (isset($single_conv->verb) AND ($single_conv->verb == "share") AND isset($single_conv->object)) { - if (is_array($single_conv->object)) - $single_conv->object = $single_conv->object[0]; - - logger("Found reshared item ".$single_conv->object->id); - - // $single_conv->object->context->conversation; - - if (isset($single_conv->object->object->id)) - $arr["uri"] = $single_conv->object->object->id; - else - $arr["uri"] = $single_conv->object->id; - - if (isset($single_conv->object->object->url)) - $plink = ostatus_convert_href($single_conv->object->object->url); - else - $plink = ostatus_convert_href($single_conv->object->url); - - if (isset($single_conv->object->object->content)) - $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->object->content)); - else - $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->content)); - - $arr["plink"] = $plink; - - $arr["created"] = $single_conv->object->published; - $arr["edited"] = $single_conv->object->published; - - $arr["author-name"] = $single_conv->object->actor->displayName; - if ($arr["owner-name"] == '') - $arr["author-name"] = $single_conv->object->actor->contact->displayName; - - $arr["author-link"] = $single_conv->object->actor->url; - $arr["author-avatar"] = $single_conv->object->actor->image->url; - - $arr["app"] = $single_conv->object->provider->displayName."#"; - //$arr["verb"] = $single_conv->object->verb; - - $arr["location"] = $single_conv->object->location->displayName; - $arr["coord"] = trim($single_conv->object->location->lat." ".$single_conv->object->location->lon); + // If the thread shouldn't be completed then store the item and go away + // Don't do a completion on liked content + if (((intval(get_config('system','ostatus_poll_interval')) == -2) AND (count($item) > 0)) OR + ($item["verb"] == ACTIVITY_LIKE) OR ($conversation_url == "")) { + $item_stored = item_store($item, true); + return($item_stored); } - if ($arr["location"] == "") - unset($arr["location"]); - - if ($arr["coord"] == "") - unset($arr["coord"]); - - // Copy fields from given item array - if (isset($item["uri"]) AND (($item["uri"] == $arr["uri"]) OR ($item["uri"] == $single_conv->id))) { - $copy_fields = array("owner-name", "owner-link", "owner-avatar", "author-name", "author-link", "author-avatar", - "gravity", "body", "object-type", "object", "verb", "created", "edited", "coord", "tag", - "title", "attach", "app", "type", "location", "contact-id", "uri"); - foreach ($copy_fields AS $field) - if (isset($item[$field])) - $arr[$field] = $item[$field]; - - //$arr["app"] .= " (OStatus)"; - } - - $newitem = item_store($arr); - if (!$newitem) { - logger("Item wasn't stored ".print_r($arr, true), LOGGER_DEBUG); - continue; - } - - if (isset($item["uri"]) AND ($item["uri"] == $arr["uri"])) { - $item = array(); - $item_stored = $newitem; - } - - logger('Stored new item '.$plink.' for parent '.$arr["parent-uri"].' under id '.$newitem, LOGGER_DEBUG); - - // Add the conversation entry (but don't fetch the whole conversation) - ostatus_store_conversation($newitem, $conversation_url); - - if ($mention) { - $u = q("SELECT `notify-flags`, `language`, `username`, `email` FROM user WHERE uid = %d LIMIT 1", intval($uid)); - $r = q("SELECT `parent` FROM `item` WHERE `id` = %d", intval($newitem)); - - notification(array( - 'type' => NOTIFY_TAGSELF, - 'notify_flags' => $u[0]["notify-flags"], - 'language' => $u[0]["language"], - 'to_name' => $u[0]["username"], - 'to_email' => $u[0]["email"], - 'uid' => $uid, - 'item' => $arr, - 'link' => $a->get_baseurl().'/display/'.urlencode(get_item_guid($newitem)), - 'source_name' => $arr["author-name"], - 'source_link' => $arr["author-link"], - 'source_photo' => $arr["author-avatar"], - 'verb' => ACTIVITY_TAG, - 'otype' => 'item', - 'parent' => $r[0]["parent"] - )); - } - - // If the newly created item is the top item then change the parent settings of the thread - // This shouldn't happen anymore. This is supposed to be absolote. - if ($arr["uri"] == $first_id) { - logger('setting new parent to id '.$newitem); - $new_parents = q("SELECT `id`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `uid` = %d AND `id` = %d LIMIT 1", - intval($uid), intval($newitem)); - if ($new_parents) - $parent = $new_parents[0]; - } - } - - if (($item_stored < 0) AND (count($item) > 0)) { - //$arr["app"] .= " (OStatus-NoConvFound)"; - $item_stored = item_store($item, true); - if ($item_stored) { - logger("Uri ".$item["uri"]." wasn't found in conversation ".$conversation_url, LOGGER_DEBUG); - ostatus_store_conversation($item_stored, $conversation_url); - } - } - - return($item_stored); -} - -function ostatus_store_conversation($itemid, $conversation_url) { - global $a; - - $conversation_url = ostatus_convert_href($conversation_url); - - $messages = q("SELECT `uid`, `parent`, `created`, `received`, `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($itemid)); - if (!$messages) - return; - $message = $messages[0]; - - // Store conversation url if not done before - $conversation = q("SELECT `url` FROM `term` WHERE `uid` = %d AND `oid` = %d AND `otype` = %d AND `type` = %d", - intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION)); - - if (!$conversation) { - $r = q("INSERT INTO `term` (`uid`, `oid`, `otype`, `type`, `term`, `url`, `created`, `received`, `guid`) VALUES (%d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", - intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), - dbesc($message["created"]), dbesc($conversation_url), dbesc($message["created"]), dbesc($message["received"]), dbesc($message["guid"])); - logger('Storing conversation url '.$conversation_url.' for id '.$itemid); - } -} - -function xml_add_element($doc, $parent, $element, $value = "", $attributes = array()) { - $element = $doc->createElement($element, xmlify($value)); - - foreach ($attributes AS $key => $value) { - $attribute = $doc->createAttribute($key); - $attribute->value = xmlify($value); - $element->appendChild($attribute); - } - - $parent->appendChild($element); -} - -function ostatus_format_picture_post($body) { - $siteinfo = get_attached_data($body); - - if (($siteinfo["type"] == "photo")) { - if (isset($siteinfo["preview"])) - $preview = $siteinfo["preview"]; - else - $preview = $siteinfo["image"]; - - // Is it a remote picture? Then make a smaller preview here - $preview = proxy_url($preview, false, PROXY_SIZE_SMALL); - - // Is it a local picture? Then make it smaller here - $preview = str_replace(array("-0.jpg", "-0.png"), array("-2.jpg", "-2.png"), $preview); - $preview = str_replace(array("-1.jpg", "-1.png"), array("-2.jpg", "-2.png"), $preview); - - if (isset($siteinfo["url"])) - $url = $siteinfo["url"]; - else - $url = $siteinfo["image"]; - - $body = trim($siteinfo["text"])." [url]".$url."[/url]\n[img]".$preview."[/img]"; - } - - return $body; -} - -function ostatus_add_header($doc, $owner) { - $a = get_app(); - - $r = q("SELECT * FROM `profile` WHERE `uid` = %d AND `is-default`", - intval($owner["uid"])); - if (!$r) - return; - - $profile = $r[0]; - - $root = $doc->createElementNS(NS_ATOM, 'feed'); - $doc->appendChild($root); - - $root->setAttribute("xmlns:thr", NS_THR); - $root->setAttribute("xmlns:georss", NS_GEORSS); - $root->setAttribute("xmlns:activity", NS_ACTIVITY); - $root->setAttribute("xmlns:media", NS_MEDIA); - $root->setAttribute("xmlns:poco", NS_POCO); - $root->setAttribute("xmlns:ostatus", NS_OSTATUS); - $root->setAttribute("xmlns:statusnet", NS_STATUSNET); - - $attributes = array("uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION); - xml_add_element($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes); - xml_add_element($doc, $root, "id", $a->get_baseurl()."/profile/".$owner["nick"]); - xml_add_element($doc, $root, "title", sprintf("%s timeline", $profile["name"])); - xml_add_element($doc, $root, "subtitle", sprintf("Updates from %s on %s", $profile["name"], $a->config["sitename"])); - xml_add_element($doc, $root, "logo", $profile["photo"]); - xml_add_element($doc, $root, "updated", datetime_convert("UTC", "UTC", "now", ATOM_TIME)); - - $author = ostatus_add_author($doc, $owner, $profile); - $root->appendChild($author); - - $attributes = array("href" => $owner["url"], "rel" => "alternate", "type" => "text/html"); - xml_add_element($doc, $root, "link", "", $attributes); - - // To-Do: We have to find out what this is - //$attributes = array("href" => $a->get_baseurl()."/sup", - // "rel" => "http://api.friendfeed.com/2008/03#sup", - // "type" => "application/json"); - //xml_add_element($doc, $root, "link", "", $attributes); - - ostatus_hublinks($doc, $root); - - $attributes = array("href" => $a->get_baseurl()."/salmon/".$owner["nick"], "rel" => "salmon"); - xml_add_element($doc, $root, "link", "", $attributes); - - $attributes = array("href" => $a->get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-replies"); - xml_add_element($doc, $root, "link", "", $attributes); - - $attributes = array("href" => $a->get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-mention"); - xml_add_element($doc, $root, "link", "", $attributes); - - $attributes = array("href" => $a->get_baseurl()."/api/statuses/user_timeline/".$owner["nick"].".atom", - "rel" => "self", "type" => "application/atom+xml"); - xml_add_element($doc, $root, "link", "", $attributes); - - return $root; -} - -function ostatus_hublinks($doc, $root) { - $a = get_app(); - $hub = get_config('system','huburl'); - - $hubxml = ''; - if(strlen($hub)) { - $hubs = explode(',', $hub); - if(count($hubs)) { - foreach($hubs as $h) { - $h = trim($h); - if(! strlen($h)) - continue; - if ($h === '[internal]') - $h = $a->get_baseurl() . '/pubsubhubbub'; - xml_add_element($doc, $root, "link", "", array("href" => $h, "rel" => "hub")); - } - } - } -} - -function ostatus_get_attachment($doc, $root, $item) { - $o = ""; - $siteinfo = get_attached_data($item["body"]); - - switch($siteinfo["type"]) { - case 'link': - $attributes = array("rel" => "enclosure", - "href" => $siteinfo["url"], - "type" => "text/html; charset=UTF-8", - "length" => "", - "title" => $siteinfo["title"]); - xml_add_element($doc, $root, "link", "", $attributes); - break; - case 'photo': - $imgdata = get_photo_info($siteinfo["image"]); - $attributes = array("rel" => "enclosure", - "href" => $siteinfo["image"], - "type" => $imgdata["mime"], - "length" => intval($imgdata["size"])); - xml_add_element($doc, $root, "link", "", $attributes); - break; - case 'video': - $attributes = array("rel" => "enclosure", - "href" => $siteinfo["url"], - "type" => "text/html; charset=UTF-8", - "length" => "", - "title" => $siteinfo["title"]); - xml_add_element($doc, $root, "link", "", $attributes); - break; - default: - break; - } - - if (($siteinfo["type"] != "photo") AND isset($siteinfo["image"])) { - $photodata = get_photo_info($siteinfo["image"]); - - $attributes = array("rel" => "preview", "href" => $siteinfo["image"], "media:width" => $photodata[0], "media:height" => $photodata[1]); - xml_add_element($doc, $root, "link", "", $attributes); - } - - - $arr = explode('[/attach],',$item['attach']); - if(count($arr)) { - foreach($arr as $r) { - $matches = false; - $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|',$r,$matches); - if($cnt) { - $attributes = array("rel" => "enclosure", - "href" => $matches[1], - "type" => $matches[3]); - - if(intval($matches[2])) - $attributes["length"] = intval($matches[2]); - - if(trim($matches[4]) != "") - $attributes["title"] = trim($matches[4]); - - xml_add_element($doc, $root, "link", "", $attributes); - } - } - } -} - -function ostatus_add_author($doc, $owner, $profile) { - $a = get_app(); - - $author = $doc->createElement("author"); - xml_add_element($doc, $author, "activity:object-type", ACTIVITY_OBJ_PERSON); - xml_add_element($doc, $author, "uri", $owner["url"]); - xml_add_element($doc, $author, "name", $profile["name"]); - - $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $owner["url"]); - xml_add_element($doc, $author, "link", "", $attributes); - - $attributes = array( - "rel" => "avatar", - "type" => "image/jpeg", // To-Do? - "media:width" => 175, - "media:height" => 175, - "href" => $profile["photo"]); - xml_add_element($doc, $author, "link", "", $attributes); - - $attributes = array( - "rel" => "avatar", - "type" => "image/jpeg", // To-Do? - "media:width" => 80, - "media:height" => 80, - "href" => $profile["thumb"]); - xml_add_element($doc, $author, "link", "", $attributes); - - xml_add_element($doc, $author, "poco:preferredUsername", $owner["nick"]); - xml_add_element($doc, $author, "poco:displayName", $profile["name"]); - xml_add_element($doc, $author, "poco:note", bbcode($profile["about"])); - - if (trim($owner["location"]) != "") { - $element = $doc->createElement("poco:address"); - xml_add_element($doc, $element, "poco:formatted", $owner["location"]); - $author->appendChild($element); - } - - if (trim($profile["homepage"]) != "") { - $urls = $doc->createElement("poco:urls"); - xml_add_element($doc, $urls, "poco:type", "homepage"); - xml_add_element($doc, $urls, "poco:value", $profile["homepage"]); - xml_add_element($doc, $urls, "poco:primary", "true"); - $author->appendChild($urls); - } - - xml_add_element($doc, $author, "followers", "", array("url" => $a->get_baseurl()."/viewcontacts/".$owner["nick"])); - xml_add_element($doc, $author, "statusnet:profile_info", "", array("local_id" => $owner["uid"])); - - return $author; -} - -/* -To-Do: Picture attachments should look like this: - -https://status.pirati.ca/attachment/572819 - + // Get the parent + $parents = q("SELECT `item`.`id`, `item`.`parent`, `item`.`uri`, `item`.`contact-id`, `item`.`type`, + `item`.`verb`, `item`.`visible` FROM `term` + STRAIGHT_JOIN `item` AS `thritem` ON `thritem`.`parent` = `term`.`oid` + STRAIGHT_JOIN `item` ON `item`.`parent` = `thritem`.`parent` + WHERE `term`.`uid` = %d AND `term`.`otype` = %d AND `term`.`type` = %d AND `term`.`url` = '%s'", + intval($uid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), dbesc($conversation_url)); + +/* 2016-10-23: The old query will be kept until we are sure that the query above is a good and fast replacement + + $parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN + (SELECT `parent` FROM `item` WHERE `id` IN + (SELECT `oid` FROM `term` WHERE `uid` = %d AND `otype` = %d AND `type` = %d AND `url` = '%s'))", + intval($uid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), dbesc($conversation_url)); */ + if ($parents) + $parent = $parents[0]; + elseif (count($item) > 0) { + $parent = $item; + $parent["type"] = "remote"; + $parent["verb"] = ACTIVITY_POST; + $parent["visible"] = 1; + } else { + // Preset the parent + $r = q("SELECT `id` FROM `contact` WHERE `self` AND `uid`=%d", $uid); + if (!$r) + return(-2); -function ostatus_entry($doc, $item, $owner, $toplevel = false) { - $a = get_app(); + $parent = array(); + $parent["id"] = 0; + $parent["parent"] = 0; + $parent["uri"] = ""; + $parent["contact-id"] = $r[0]["id"]; + $parent["type"] = "remote"; + $parent["verb"] = ACTIVITY_POST; + $parent["visible"] = 1; + } - if (!$toplevel) { - $entry = $doc->createElement("entry"); - $title = sprintf("New note by %s", $owner["nick"]); - } else { - $entry = $doc->createElementNS(NS_ATOM, "entry"); + $conv = str_replace("/conversation/", "/api/statusnet/conversation/", $conversation_url).".as"; + $pageno = 1; + $items = array(); - $entry->setAttribute("xmlns:thr", NS_THR); - $entry->setAttribute("xmlns:georss", NS_GEORSS); - $entry->setAttribute("xmlns:activity", NS_ACTIVITY); - $entry->setAttribute("xmlns:media", NS_MEDIA); - $entry->setAttribute("xmlns:poco", NS_POCO); - $entry->setAttribute("xmlns:ostatus", NS_OSTATUS); - $entry->setAttribute("xmlns:statusnet", NS_STATUSNET); + logger('fetching conversation url '.$conv.' (Self: '.$self.') for user '.$uid); - $r = q("SELECT * FROM `profile` WHERE `uid` = %d AND `is-default`", - intval($owner["uid"])); + do { + $conv_arr = z_fetch_url($conv."?page=".$pageno); + + // If it is a non-ssl site and there is an error, then try ssl or vice versa + if (!$conv_arr["success"] AND (substr($conv, 0, 7) == "http://")) { + $conv = str_replace("http://", "https://", $conv); + $conv_as = fetch_url($conv."?page=".$pageno); + } elseif (!$conv_arr["success"] AND (substr($conv, 0, 8) == "https://")) { + $conv = str_replace("https://", "http://", $conv); + $conv_as = fetch_url($conv."?page=".$pageno); + } else + $conv_as = $conv_arr["body"]; + + $conv_as = str_replace(',"statusnet:notice_info":', ',"statusnet_notice_info":', $conv_as); + $conv_as = json_decode($conv_as); + + $no_of_items = sizeof($items); + + if (@is_array($conv_as->items)) + foreach ($conv_as->items AS $single_item) + $items[$single_item->id] = $single_item; + + if ($no_of_items == sizeof($items)) + break; + + $pageno++; + + } while (true); + + logger('fetching conversation done. Found '.count($items).' items'); + + if (!sizeof($items)) { + if (count($item) > 0) { + $item_stored = item_store($item, true); + + if ($item_stored) { + logger("Conversation ".$conversation_url." couldn't be fetched. Item uri ".$item["uri"]." stored: ".$item_stored, LOGGER_DEBUG); + self::store_conversation($item_id, $conversation_url); + } + + return($item_stored); + } else + return(-3); + } + + $items = array_reverse($items); + + $r = q("SELECT `nurl` FROM `contact` WHERE `uid` = %d AND `self`", intval($uid)); + $importer = $r[0]; + + $new_parent = true; + + foreach ($items as $single_conv) { + + // Update the gcontact table + self::conv_fetch_actor($single_conv->actor); + + // Test - remove before flight + //$tempfile = tempnam(get_temppath(), "conversation"); + //file_put_contents($tempfile, json_encode($single_conv)); + + $mention = false; + + if (isset($single_conv->object->id)) + $single_conv->id = $single_conv->object->id; + + $plink = self::convert_href($single_conv->id); + if (isset($single_conv->object->url)) + $plink = self::convert_href($single_conv->object->url); + + if (@!$single_conv->id) + continue; + + logger("Got id ".$single_conv->id, LOGGER_DEBUG); + + if ($first_id == "") { + $first_id = $single_conv->id; + + // The first post of the conversation isn't our first post. There are three options: + // 1. Our conversation hasn't the "real" thread starter + // 2. This first post is a post inside our thread + // 3. This first post is a post inside another thread + if (($first_id != $parent["uri"]) AND ($parent["uri"] != "")) { + + $new_parent = true; + + $new_parents = q("SELECT `id`, `parent`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `id` IN + (SELECT `parent` FROM `item` + WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s')) LIMIT 1", + intval($uid), dbesc($first_id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); + if ($new_parents) { + if ($new_parents[0]["parent"] == $parent["parent"]) { + // Option 2: This post is already present inside our thread - but not as thread starter + logger("Option 2: uri present in our thread: ".$first_id, LOGGER_DEBUG); + $first_id = $parent["uri"]; + } else { + // Option 3: Not so good. We have mixed parents. We have to see how to clean this up. + // For now just take the new parent. + $parent = $new_parents[0]; + $first_id = $parent["uri"]; + logger("Option 3: mixed parents for uri ".$first_id, LOGGER_DEBUG); + } + } else { + // Option 1: We hadn't got the real thread starter + // We have to clean up our existing messages. + $parent["id"] = 0; + $parent["uri"] = $first_id; + logger("Option 1: we have a new parent: ".$first_id, LOGGER_DEBUG); + } + } elseif ($parent["uri"] == "") { + $parent["id"] = 0; + $parent["uri"] = $first_id; + } + } + + $parent_uri = $parent["uri"]; + + // "context" only seems to exist on older servers + if (isset($single_conv->context->inReplyTo->id)) { + $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", + intval($uid), dbesc($single_conv->context->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); + if ($parent_exists) + $parent_uri = $single_conv->context->inReplyTo->id; + } + + // This is the current way + if (isset($single_conv->object->inReplyTo->id)) { + $parent_exists = q("SELECT `id` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", + intval($uid), dbesc($single_conv->object->inReplyTo->id), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); + if ($parent_exists) + $parent_uri = $single_conv->object->inReplyTo->id; + } + + $message_exists = q("SELECT `id`, `parent`, `uri` FROM `item` WHERE `uid` = %d AND `uri` = '%s' AND `network` IN ('%s','%s') LIMIT 1", + intval($uid), dbesc($single_conv->id), + dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); + if ($message_exists) { + logger("Message ".$single_conv->id." already existed on the system", LOGGER_DEBUG); + + if ($parent["id"] != 0) { + $existing_message = $message_exists[0]; + + // We improved the way we fetch OStatus messages, this shouldn't happen very often now + /// @TODO We have to change the shadow copies as well. This way here is really ugly. + if ($existing_message["parent"] != $parent["id"]) { + logger('updating id '.$existing_message["id"].' with parent '.$existing_message["parent"].' to parent '.$parent["id"].' uri '.$parent["uri"].' thread '.$parent_uri, LOGGER_DEBUG); + + // Update the parent id of the selected item + $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `id` = %d", + intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["id"])); + + // Update the parent uri in the thread - but only if it points to itself + $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE `id` = %d AND `uri` = `thr-parent`", + dbesc($parent_uri), intval($existing_message["id"])); + + // try to change all items of the same parent + $r = q("UPDATE `item` SET `parent` = %d, `parent-uri` = '%s' WHERE `parent` = %d", + intval($parent["id"]), dbesc($parent["uri"]), intval($existing_message["parent"])); + + // Update the parent uri in the thread - but only if it points to itself + $r = q("UPDATE `item` SET `thr-parent` = '%s' WHERE (`parent` = %d) AND (`uri` = `thr-parent`)", + dbesc($parent["uri"]), intval($existing_message["parent"])); + + // Now delete the thread + delete_thread($existing_message["parent"]); + } + } + + // The item we are having on the system is the one that we wanted to store via the item array + if (isset($item["uri"]) AND ($item["uri"] == $existing_message["uri"])) { + $item = array(); + $item_stored = 0; + } + + continue; + } + + if (is_array($single_conv->to)) + foreach($single_conv->to AS $to) + if ($importer["nurl"] == normalise_link($to->id)) + $mention = true; + + $actor = $single_conv->actor->id; + if (isset($single_conv->actor->url)) + $actor = $single_conv->actor->url; + + $details = self::get_actor_details($actor, $uid, $parent["contact-id"]); + + // Do we only want to import threads that were started by our contacts? + if ($details["not_following"] AND $new_parent AND get_config('system','ostatus_full_threads')) { + logger("Don't import uri ".$first_id." because user ".$uid." doesn't follow the person ".$actor, LOGGER_DEBUG); + continue; + } + + $arr = array(); + $arr["network"] = $details["network"]; + $arr["uri"] = $single_conv->id; + $arr["plink"] = $plink; + $arr["uid"] = $uid; + $arr["contact-id"] = $details["contact_id"]; + $arr["parent-uri"] = $parent_uri; + $arr["created"] = $single_conv->published; + $arr["edited"] = $single_conv->published; + $arr["owner-name"] = $single_conv->actor->displayName; + if ($arr["owner-name"] == '') + $arr["owner-name"] = $single_conv->actor->contact->displayName; + if ($arr["owner-name"] == '') + $arr["owner-name"] = $single_conv->actor->portablecontacts_net->displayName; + + $arr["owner-link"] = $actor; + $arr["owner-avatar"] = $single_conv->actor->image->url; + $arr["author-name"] = $arr["owner-name"]; + $arr["author-link"] = $actor; + $arr["author-avatar"] = $single_conv->actor->image->url; + $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->content)); + + if (isset($single_conv->status_net->notice_info->source)) + $arr["app"] = strip_tags($single_conv->status_net->notice_info->source); + elseif (isset($single_conv->statusnet->notice_info->source)) + $arr["app"] = strip_tags($single_conv->statusnet->notice_info->source); + elseif (isset($single_conv->statusnet_notice_info->source)) + $arr["app"] = strip_tags($single_conv->statusnet_notice_info->source); + elseif (isset($single_conv->provider->displayName)) + $arr["app"] = $single_conv->provider->displayName; + else + $arr["app"] = "OStatus"; + + + $arr["object"] = json_encode($single_conv); + $arr["verb"] = $parent["verb"]; + $arr["visible"] = $parent["visible"]; + $arr["location"] = $single_conv->location->displayName; + $arr["coord"] = trim($single_conv->location->lat." ".$single_conv->location->lon); + + // Is it a reshared item? + if (isset($single_conv->verb) AND ($single_conv->verb == "share") AND isset($single_conv->object)) { + if (is_array($single_conv->object)) + $single_conv->object = $single_conv->object[0]; + + logger("Found reshared item ".$single_conv->object->id); + + // $single_conv->object->context->conversation; + + if (isset($single_conv->object->object->id)) + $arr["uri"] = $single_conv->object->object->id; + else + $arr["uri"] = $single_conv->object->id; + + if (isset($single_conv->object->object->url)) + $plink = self::convert_href($single_conv->object->object->url); + else + $plink = self::convert_href($single_conv->object->url); + + if (isset($single_conv->object->object->content)) + $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->object->content)); + else + $arr["body"] = add_page_info_to_body(html2bbcode($single_conv->object->content)); + + $arr["plink"] = $plink; + + $arr["created"] = $single_conv->object->published; + $arr["edited"] = $single_conv->object->published; + + $arr["author-name"] = $single_conv->object->actor->displayName; + if ($arr["owner-name"] == '') + $arr["author-name"] = $single_conv->object->actor->contact->displayName; + + $arr["author-link"] = $single_conv->object->actor->url; + $arr["author-avatar"] = $single_conv->object->actor->image->url; + + $arr["app"] = $single_conv->object->provider->displayName."#"; + //$arr["verb"] = $single_conv->object->verb; + + $arr["location"] = $single_conv->object->location->displayName; + $arr["coord"] = trim($single_conv->object->location->lat." ".$single_conv->object->location->lon); + } + + if ($arr["location"] == "") + unset($arr["location"]); + + if ($arr["coord"] == "") + unset($arr["coord"]); + + // Copy fields from given item array + if (isset($item["uri"]) AND (($item["uri"] == $arr["uri"]) OR ($item["uri"] == $single_conv->id))) { + $copy_fields = array("owner-name", "owner-link", "owner-avatar", "author-name", "author-link", "author-avatar", + "gravity", "body", "object-type", "object", "verb", "created", "edited", "coord", "tag", + "title", "attach", "app", "type", "location", "contact-id", "uri"); + foreach ($copy_fields AS $field) + if (isset($item[$field])) + $arr[$field] = $item[$field]; + + } + + $newitem = item_store($arr); + if (!$newitem) { + logger("Item wasn't stored ".print_r($arr, true), LOGGER_DEBUG); + continue; + } + + if (isset($item["uri"]) AND ($item["uri"] == $arr["uri"])) { + $item = array(); + $item_stored = $newitem; + } + + logger('Stored new item '.$plink.' for parent '.$arr["parent-uri"].' under id '.$newitem, LOGGER_DEBUG); + + // Add the conversation entry (but don't fetch the whole conversation) + self::store_conversation($newitem, $conversation_url); + + // If the newly created item is the top item then change the parent settings of the thread + // This shouldn't happen anymore. This is supposed to be absolote. + if ($arr["uri"] == $first_id) { + logger('setting new parent to id '.$newitem); + $new_parents = q("SELECT `id`, `uri`, `contact-id`, `type`, `verb`, `visible` FROM `item` WHERE `uid` = %d AND `id` = %d LIMIT 1", + intval($uid), intval($newitem)); + if ($new_parents) + $parent = $new_parents[0]; + } + } + + if (($item_stored < 0) AND (count($item) > 0)) { + + if (get_config('system','ostatus_full_threads')) { + $details = self::get_actor_details($item["owner-link"], $uid, $item["contact-id"]); + if ($details["not_following"]) { + logger("Don't import uri ".$item["uri"]." because user ".$uid." doesn't follow the person ".$item["owner-link"], LOGGER_DEBUG); + return false; + } + } + + $item_stored = item_store($item, true); + if ($item_stored) { + logger("Uri ".$item["uri"]." wasn't found in conversation ".$conversation_url, LOGGER_DEBUG); + self::store_conversation($item_stored, $conversation_url); + } + } + + return($item_stored); + } + + /** + * @brief Stores conversation data into the database + * + * @param integer $itemid The id of the item + * @param string $conversation_url The uri of the conversation + */ + private function store_conversation($itemid, $conversation_url) { + + $conversation_url = self::convert_href($conversation_url); + + $messages = q("SELECT `uid`, `parent`, `created`, `received`, `guid` FROM `item` WHERE `id` = %d LIMIT 1", intval($itemid)); + if (!$messages) + return; + $message = $messages[0]; + + // Store conversation url if not done before + $conversation = q("SELECT `url` FROM `term` WHERE `uid` = %d AND `oid` = %d AND `otype` = %d AND `type` = %d", + intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION)); + + if (!$conversation) { + $r = q("INSERT INTO `term` (`uid`, `oid`, `otype`, `type`, `term`, `url`, `created`, `received`, `guid`) VALUES (%d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s')", + intval($message["uid"]), intval($itemid), intval(TERM_OBJ_POST), intval(TERM_CONVERSATION), + dbesc($message["created"]), dbesc($conversation_url), dbesc($message["created"]), dbesc($message["received"]), dbesc($message["guid"])); + logger('Storing conversation url '.$conversation_url.' for id '.$itemid); + } + } + + /** + * @brief Checks if the current post is a reshare + * + * @param array $item The item array of thw post + * + * @return string The guid if the post is a reshare + */ + private function get_reshared_guid($item) { + $body = trim($item["body"]); + + // Skip if it isn't a pure repeated messages + // Does it start with a share? + if (strpos($body, "[share") > 0) + return(""); + + // Does it end with a share? + if (strlen($body) > (strrpos($body, "[/share]") + 8)) + return(""); + + $attributes = preg_replace("/\[share(.*?)\]\s?(.*?)\s?\[\/share\]\s?/ism","$1",$body); + // Skip if there is no shared message in there + if ($body == $attributes) + return(false); + + $guid = ""; + preg_match("/guid='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $guid = $matches[1]; + + preg_match('/guid="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $guid = $matches[1]; + + return $guid; + } + + /** + * @brief Cleans the body of a post if it contains picture links + * + * @param string $body The body + * + * @return string The cleaned body + */ + private function format_picture_post($body) { + $siteinfo = get_attached_data($body); + + if (($siteinfo["type"] == "photo")) { + if (isset($siteinfo["preview"])) + $preview = $siteinfo["preview"]; + else + $preview = $siteinfo["image"]; + + // Is it a remote picture? Then make a smaller preview here + $preview = proxy_url($preview, false, PROXY_SIZE_SMALL); + + // Is it a local picture? Then make it smaller here + $preview = str_replace(array("-0.jpg", "-0.png"), array("-2.jpg", "-2.png"), $preview); + $preview = str_replace(array("-1.jpg", "-1.png"), array("-2.jpg", "-2.png"), $preview); + + if (isset($siteinfo["url"])) + $url = $siteinfo["url"]; + else + $url = $siteinfo["image"]; + + $body = trim($siteinfo["text"])." [url]".$url."[/url]\n[img]".$preview."[/img]"; + } + + return $body; + } + + /** + * @brief Adds the header elements to the XML document + * + * @param object $doc XML document + * @param array $owner Contact data of the poster + * + * @return object header root element + */ + private function add_header($doc, $owner) { + + $a = get_app(); + + $root = $doc->createElementNS(NAMESPACE_ATOM1, 'feed'); + $doc->appendChild($root); + + $root->setAttribute("xmlns:thr", NAMESPACE_THREAD); + $root->setAttribute("xmlns:georss", NAMESPACE_GEORSS); + $root->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY); + $root->setAttribute("xmlns:media", NAMESPACE_MEDIA); + $root->setAttribute("xmlns:poco", NAMESPACE_POCO); + $root->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS); + $root->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET); + + $attributes = array("uri" => "https://friendi.ca", "version" => FRIENDICA_VERSION."-".DB_UPDATE_VERSION); + xml::add_element($doc, $root, "generator", FRIENDICA_PLATFORM, $attributes); + xml::add_element($doc, $root, "id", App::get_baseurl()."/profile/".$owner["nick"]); + xml::add_element($doc, $root, "title", sprintf("%s timeline", $owner["name"])); + xml::add_element($doc, $root, "subtitle", sprintf("Updates from %s on %s", $owner["name"], $a->config["sitename"])); + xml::add_element($doc, $root, "logo", $owner["photo"]); + xml::add_element($doc, $root, "updated", datetime_convert("UTC", "UTC", "now", ATOM_TIME)); + + $author = self::add_author($doc, $owner); + $root->appendChild($author); + + $attributes = array("href" => $owner["url"], "rel" => "alternate", "type" => "text/html"); + xml::add_element($doc, $root, "link", "", $attributes); + + /// @TODO We have to find out what this is + /// $attributes = array("href" => App::get_baseurl()."/sup", + /// "rel" => "http://api.friendfeed.com/2008/03#sup", + /// "type" => "application/json"); + /// xml::add_element($doc, $root, "link", "", $attributes); + + self::hublinks($doc, $root); + + $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "salmon"); + xml::add_element($doc, $root, "link", "", $attributes); + + $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-replies"); + xml::add_element($doc, $root, "link", "", $attributes); + + $attributes = array("href" => App::get_baseurl()."/salmon/".$owner["nick"], "rel" => "http://salmon-protocol.org/ns/salmon-mention"); + xml::add_element($doc, $root, "link", "", $attributes); + + $attributes = array("href" => App::get_baseurl()."/api/statuses/user_timeline/".$owner["nick"].".atom", + "rel" => "self", "type" => "application/atom+xml"); + xml::add_element($doc, $root, "link", "", $attributes); + + return $root; + } + + /** + * @brief Add the link to the push hubs to the XML document + * + * @param object $doc XML document + * @param object $root XML root element where the hub links are added + */ + public static function hublinks($doc, $root) { + $hub = get_config('system','huburl'); + + $hubxml = ''; + if(strlen($hub)) { + $hubs = explode(',', $hub); + if(count($hubs)) { + foreach($hubs as $h) { + $h = trim($h); + if(! strlen($h)) + continue; + if ($h === '[internal]') + $h = App::get_baseurl() . '/pubsubhubbub'; + xml::add_element($doc, $root, "link", "", array("href" => $h, "rel" => "hub")); + } + } + } + } + + /** + * @brief Adds attachement data to the XML document + * + * @param object $doc XML document + * @param object $root XML root element where the hub links are added + * @param array $item Data of the item that is to be posted + */ + private function get_attachment($doc, $root, $item) { + $o = ""; + $siteinfo = get_attached_data($item["body"]); + + switch($siteinfo["type"]) { + case 'link': + $attributes = array("rel" => "enclosure", + "href" => $siteinfo["url"], + "type" => "text/html; charset=UTF-8", + "length" => "", + "title" => $siteinfo["title"]); + xml::add_element($doc, $root, "link", "", $attributes); + break; + case 'photo': + $imgdata = get_photo_info($siteinfo["image"]); + $attributes = array("rel" => "enclosure", + "href" => $siteinfo["image"], + "type" => $imgdata["mime"], + "length" => intval($imgdata["size"])); + xml::add_element($doc, $root, "link", "", $attributes); + break; + case 'video': + $attributes = array("rel" => "enclosure", + "href" => $siteinfo["url"], + "type" => "text/html; charset=UTF-8", + "length" => "", + "title" => $siteinfo["title"]); + xml::add_element($doc, $root, "link", "", $attributes); + break; + default: + break; + } + + if (($siteinfo["type"] != "photo") AND isset($siteinfo["image"])) { + $photodata = get_photo_info($siteinfo["image"]); + + $attributes = array("rel" => "preview", "href" => $siteinfo["image"], "media:width" => $photodata[0], "media:height" => $photodata[1]); + xml::add_element($doc, $root, "link", "", $attributes); + } + + + $arr = explode('[/attach],',$item['attach']); + if(count($arr)) { + foreach($arr as $r) { + $matches = false; + $cnt = preg_match('|\[attach\]href=\"(.*?)\" length=\"(.*?)\" type=\"(.*?)\" title=\"(.*?)\"|',$r,$matches); + if($cnt) { + $attributes = array("rel" => "enclosure", + "href" => $matches[1], + "type" => $matches[3]); + + if(intval($matches[2])) + $attributes["length"] = intval($matches[2]); + + if(trim($matches[4]) != "") + $attributes["title"] = trim($matches[4]); + + xml::add_element($doc, $root, "link", "", $attributes); + } + } + } + } + + /** + * @brief Adds the author element to the XML document + * + * @param object $doc XML document + * @param array $owner Contact data of the poster + * + * @return object author element + */ + private function add_author($doc, $owner) { + + $r = q("SELECT `homepage` FROM `profile` WHERE `uid` = %d AND `is-default` LIMIT 1", intval($owner["uid"])); + if ($r) + $profile = $r[0]; + + $author = $doc->createElement("author"); + xml::add_element($doc, $author, "activity:object-type", ACTIVITY_OBJ_PERSON); + xml::add_element($doc, $author, "uri", $owner["url"]); + xml::add_element($doc, $author, "name", $owner["name"]); + xml::add_element($doc, $author, "summary", bbcode($owner["about"], false, false, 7)); + + $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $owner["url"]); + xml::add_element($doc, $author, "link", "", $attributes); + + $attributes = array( + "rel" => "avatar", + "type" => "image/jpeg", // To-Do? + "media:width" => 175, + "media:height" => 175, + "href" => $owner["photo"]); + xml::add_element($doc, $author, "link", "", $attributes); + + if (isset($owner["thumb"])) { + $attributes = array( + "rel" => "avatar", + "type" => "image/jpeg", // To-Do? + "media:width" => 80, + "media:height" => 80, + "href" => $owner["thumb"]); + xml::add_element($doc, $author, "link", "", $attributes); + } + + xml::add_element($doc, $author, "poco:preferredUsername", $owner["nick"]); + xml::add_element($doc, $author, "poco:displayName", $owner["name"]); + xml::add_element($doc, $author, "poco:note", bbcode($owner["about"], false, false, 7)); + + if (trim($owner["location"]) != "") { + $element = $doc->createElement("poco:address"); + xml::add_element($doc, $element, "poco:formatted", $owner["location"]); + $author->appendChild($element); + } + + if (trim($profile["homepage"]) != "") { + $urls = $doc->createElement("poco:urls"); + xml::add_element($doc, $urls, "poco:type", "homepage"); + xml::add_element($doc, $urls, "poco:value", $profile["homepage"]); + xml::add_element($doc, $urls, "poco:primary", "true"); + $author->appendChild($urls); + } + + if (count($profile)) { + xml::add_element($doc, $author, "followers", "", array("url" => App::get_baseurl()."/viewcontacts/".$owner["nick"])); + xml::add_element($doc, $author, "statusnet:profile_info", "", array("local_id" => $owner["uid"])); + } + + return $author; + } + + /** + * @TODO Picture attachments should look like this: + * https://status.pirati.ca/attachment/572819 + * + */ + + /** + * @brief Returns the given activity if present - otherwise returns the "post" activity + * + * @param array $item Data of the item that is to be posted + * + * @return string activity + */ + function construct_verb($item) { + if ($item['verb']) + return $item['verb']; + return ACTIVITY_POST; + } + + /** + * @brief Returns the given object type if present - otherwise returns the "note" object type + * + * @param array $item Data of the item that is to be posted + * + * @return string Object type + */ + function construct_objecttype($item) { + if (in_array($item['object-type'], array(ACTIVITY_OBJ_NOTE, ACTIVITY_OBJ_COMMENT))) + return $item['object-type']; + return ACTIVITY_OBJ_NOTE; + } + + /** + * @brief Adds an entry element to the XML document + * + * @param object $doc XML document + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param bool $toplevel + * + * @return object Entry element + */ + private function entry($doc, $item, $owner, $toplevel = false) { + $repeated_guid = self::get_reshared_guid($item); + if ($repeated_guid != "") + $xml = self::reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel); + + if ($xml) + return $xml; + + if ($item["verb"] == ACTIVITY_LIKE) { + return self::like_entry($doc, $item, $owner, $toplevel); + } elseif (in_array($item["verb"], array(ACTIVITY_FOLLOW, NAMESPACE_OSTATUS."/unfollow"))) { + return self::follow_entry($doc, $item, $owner, $toplevel); + } else { + return self::note_entry($doc, $item, $owner, $toplevel); + } + } + + /** + * @brief Adds a source entry to the XML document + * + * @param object $doc XML document + * @param array $contact Array of the contact that is added + * + * @return object Source element + */ + private function source_entry($doc, $contact) { + $source = $doc->createElement("source"); + xml::add_element($doc, $source, "id", $contact["poll"]); + xml::add_element($doc, $source, "title", $contact["name"]); + xml::add_element($doc, $source, "link", "", array("rel" => "alternate", + "type" => "text/html", + "href" => $contact["alias"])); + xml::add_element($doc, $source, "link", "", array("rel" => "self", + "type" => "application/atom+xml", + "href" => $contact["poll"])); + xml::add_element($doc, $source, "icon", $contact["photo"]); + xml::add_element($doc, $source, "updated", datetime_convert("UTC","UTC",$contact["success_update"]."+00:00",ATOM_TIME)); + + return $source; + } + + /** + * @brief Fetches contact data from the contact or the gcontact table + * + * @param string $url URL of the contact + * @param array $owner Contact data of the poster + * + * @return array Contact array + */ + private function contact_entry($url, $owner) { + + $r = q("SELECT * FROM `contact` WHERE `nurl` = '%s' AND `uid` IN (0, %d) ORDER BY `uid` DESC LIMIT 1", + dbesc(normalise_link($url)), intval($owner["uid"])); + if ($r) { + $contact = $r[0]; + $contact["uid"] = -1; + } + + if (!$r) { + $r = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1", + dbesc(normalise_link($url))); + if ($r) { + $contact = $r[0]; + $contact["uid"] = -1; + $contact["success_update"] = $contact["updated"]; + } + } + + if (!$r) + $contact = owner; + + if (!isset($contact["poll"])) { + $data = probe_url($url); + $contact["poll"] = $data["poll"]; + + if (!$contact["alias"]) + $contact["alias"] = $data["alias"]; + } + + if (!isset($contact["alias"])) + $contact["alias"] = $contact["url"]; + + return $contact; + } + + /** + * @brief Adds an entry element with reshared content + * + * @param object $doc XML document + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param $repeated_guid + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * + * @return object Entry element + */ + private function reshare_entry($doc, $item, $owner, $repeated_guid, $toplevel) { + + if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) { + logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG); + } + + $title = self::entry_header($doc, $entry, $owner, $toplevel); + + $r = q("SELECT * FROM `item` WHERE `uid` = %d AND `guid` = '%s' AND NOT `private` AND `network` IN ('%s', '%s', '%s') LIMIT 1", + intval($owner["uid"]), dbesc($repeated_guid), + dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS)); + if ($r) + $repeated_item = $r[0]; + else + return false; + + $contact = self::contact_entry($repeated_item['author-link'], $owner); + + $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']); + + $title = $owner["nick"]." repeated a notice by ".$contact["nick"]; + + self::entry_content($doc, $entry, $item, $owner, $title, ACTIVITY_SHARE, false); + + $as_object = $doc->createElement("activity:object"); + + xml::add_element($doc, $as_object, "activity:object-type", NAMESPACE_ACTIVITY_SCHEMA."activity"); + + self::entry_content($doc, $as_object, $repeated_item, $owner, "", "", false); + + $author = self::add_author($doc, $contact); + $as_object->appendChild($author); + + $as_object2 = $doc->createElement("activity:object"); + + xml::add_element($doc, $as_object2, "activity:object-type", self::construct_objecttype($repeated_item)); + + $title = sprintf("New comment by %s", $contact["nick"]); + + self::entry_content($doc, $as_object2, $repeated_item, $owner, $title); + + $as_object->appendChild($as_object2); + + self::entry_footer($doc, $as_object, $item, $owner, false); + + $source = self::source_entry($doc, $contact); + + $as_object->appendChild($source); + + $entry->appendChild($as_object); + + self::entry_footer($doc, $entry, $item, $owner); + + return $entry; + } + + /** + * @brief Adds an entry element with a "like" + * + * @param object $doc XML document + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * + * @return object Entry element with "like" + */ + private function like_entry($doc, $item, $owner, $toplevel) { + + if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) { + logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG); + } + + $title = self::entry_header($doc, $entry, $owner, $toplevel); + + $verb = NAMESPACE_ACTIVITY_SCHEMA."favorite"; + self::entry_content($doc, $entry, $item, $owner, "Favorite", $verb, false); + + $as_object = $doc->createElement("activity:object"); + + $parent = q("SELECT * FROM `item` WHERE `uri` = '%s' AND `uid` = %d", + dbesc($item["thr-parent"]), intval($item["uid"])); + $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']); + + xml::add_element($doc, $as_object, "activity:object-type", self::construct_objecttype($parent[0])); + + self::entry_content($doc, $as_object, $parent[0], $owner, "New entry"); + + $entry->appendChild($as_object); + + self::entry_footer($doc, $entry, $item, $owner); + + return $entry; + } + + /** + * @brief Adds the person object element to the XML document + * + * @param object $doc XML document + * @param array $owner Contact data of the poster + * @param array $contact Contact data of the target + * + * @return object author element + */ + private function add_person_object($doc, $owner, $contact) { + + $object = $doc->createElement("activity:object"); + xml::add_element($doc, $object, "activity:object-type", ACTIVITY_OBJ_PERSON); + + if ($contact['network'] == NETWORK_PHANTOM) { + xml::add_element($doc, $object, "id", $contact['url']); + return $object; + } + + xml::add_element($doc, $object, "id", $contact["alias"]); + xml::add_element($doc, $object, "title", $contact["nick"]); + + $attributes = array("rel" => "alternate", "type" => "text/html", "href" => $contact["url"]); + xml::add_element($doc, $object, "link", "", $attributes); + + $attributes = array( + "rel" => "avatar", + "type" => "image/jpeg", // To-Do? + "media:width" => 175, + "media:height" => 175, + "href" => $contact["photo"]); + xml::add_element($doc, $object, "link", "", $attributes); + + xml::add_element($doc, $object, "poco:preferredUsername", $contact["nick"]); + xml::add_element($doc, $object, "poco:displayName", $contact["name"]); + + if (trim($contact["location"]) != "") { + $element = $doc->createElement("poco:address"); + xml::add_element($doc, $element, "poco:formatted", $contact["location"]); + $object->appendChild($element); + } + + return $object; + } + + /** + * @brief Adds a follow/unfollow entry element + * + * @param object $doc XML document + * @param array $item Data of the follow/unfollow message + * @param array $owner Contact data of the poster + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * + * @return object Entry element + */ + private function follow_entry($doc, $item, $owner, $toplevel) { + + $item["id"] = $item["parent"] = 0; + $item["created"] = $item["edited"] = date("c"); + $item["private"] = true; + + $contact = Probe::uri($item['follow']); + + if ($contact['alias'] == '') { + $contact['alias'] = $contact["url"]; + } else { + $item['follow'] = $contact['alias']; + } + + $r = q("SELECT `id` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'", + intval($owner['uid']), dbesc(normalise_link($contact["url"]))); + + if (dbm::is_result($r)) { + $connect_id = $r[0]['id']; + } else { + $connect_id = 0; + } + + if ($item['verb'] == ACTIVITY_FOLLOW) { + $message = t('%s is now following %s.'); + $title = t('following'); + $action = "subscription"; + } else { + $message = t('%s stopped following %s.'); + $title = t('stopped following'); + $action = "unfollow"; + } + + $item["uri"] = $item['parent-uri'] = $item['thr-parent'] = + 'tag:'.get_app()->get_hostname(). + ','.date('Y-m-d').':'.$action.':'.$owner['uid']. + ':person:'.$connect_id.':'.$item['created']; + + $item["body"] = sprintf($message, $owner["nick"], $contact["nick"]); + + self::entry_header($doc, $entry, $owner, $toplevel); + + self::entry_content($doc, $entry, $item, $owner, $title); + + $object = self::add_person_object($doc, $owner, $contact); + $entry->appendChild($object); + + self::entry_footer($doc, $entry, $item, $owner); + + return $entry; + } + + /** + * @brief Adds a regular entry element + * + * @param object $doc XML document + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * + * @return object Entry element + */ + private function note_entry($doc, $item, $owner, $toplevel) { + + if (($item["id"] != $item["parent"]) AND (normalise_link($item["author-link"]) != normalise_link($owner["url"]))) { + logger("OStatus entry is from author ".$owner["url"]." - not from ".$item["author-link"].". Quitting.", LOGGER_DEBUG); + } + + $title = self::entry_header($doc, $entry, $owner, $toplevel); + + xml::add_element($doc, $entry, "activity:object-type", ACTIVITY_OBJ_NOTE); + + self::entry_content($doc, $entry, $item, $owner, $title); + + self::entry_footer($doc, $entry, $item, $owner); + + return $entry; + } + + /** + * @brief Adds a header element to the XML document + * + * @param object $doc XML document + * @param object $entry The entry element where the elements are added + * @param array $owner Contact data of the poster + * @param bool $toplevel Is it for en entry element (false) or a feed entry (true)? + * + * @return string The title for the element + */ + private function entry_header($doc, &$entry, $owner, $toplevel) { + /// @todo Check if this title stuff is really needed (I guess not) + if (!$toplevel) { + $entry = $doc->createElement("entry"); + $title = sprintf("New note by %s", $owner["nick"]); + } else { + $entry = $doc->createElementNS(NAMESPACE_ATOM1, "entry"); + + $entry->setAttribute("xmlns:thr", NAMESPACE_THREAD); + $entry->setAttribute("xmlns:georss", NAMESPACE_GEORSS); + $entry->setAttribute("xmlns:activity", NAMESPACE_ACTIVITY); + $entry->setAttribute("xmlns:media", NAMESPACE_MEDIA); + $entry->setAttribute("xmlns:poco", NAMESPACE_POCO); + $entry->setAttribute("xmlns:ostatus", NAMESPACE_OSTATUS); + $entry->setAttribute("xmlns:statusnet", NAMESPACE_STATUSNET); + + $author = self::add_author($doc, $owner); + $entry->appendChild($author); + + $title = sprintf("New comment by %s", $owner["nick"]); + } + return $title; + } + + /** + * @brief Adds elements to the XML document + * + * @param object $doc XML document + * @param object $entry Entry element where the content is added + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param string $title Title for the post + * @param string $verb The activity verb + * @param bool $complete Add the "status_net" element? + */ + private function entry_content($doc, $entry, $item, $owner, $title, $verb = "", $complete = true) { + + if ($verb == "") + $verb = self::construct_verb($item); + + xml::add_element($doc, $entry, "id", $item["uri"]); + xml::add_element($doc, $entry, "title", $title); + + $body = self::format_picture_post($item['body']); + + if ($item['title'] != "") + $body = "[b]".$item['title']."[/b]\n\n".$body; + + $body = bbcode($body, false, false, 7); + + xml::add_element($doc, $entry, "content", $body, array("type" => "html")); + + xml::add_element($doc, $entry, "link", "", array("rel" => "alternate", "type" => "text/html", + "href" => App::get_baseurl()."/display/".$item["guid"])); + + if ($complete AND ($item["id"] > 0)) + xml::add_element($doc, $entry, "status_net", "", array("notice_id" => $item["id"])); + + xml::add_element($doc, $entry, "activity:verb", $verb); + + xml::add_element($doc, $entry, "published", datetime_convert("UTC","UTC",$item["created"]."+00:00",ATOM_TIME)); + xml::add_element($doc, $entry, "updated", datetime_convert("UTC","UTC",$item["edited"]."+00:00",ATOM_TIME)); + } + + /** + * @brief Adds the elements at the foot of an entry to the XML document + * + * @param object $doc XML document + * @param object $entry The entry element where the elements are added + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * @param $complete + */ + private function entry_footer($doc, $entry, $item, $owner, $complete = true) { + + $mentioned = array(); + + if (($item['parent'] != $item['id']) || ($item['parent-uri'] !== $item['uri']) || (($item['thr-parent'] !== '') && ($item['thr-parent'] !== $item['uri']))) { + $parent = q("SELECT `guid`, `author-link`, `owner-link` FROM `item` WHERE `id` = %d", intval($item["parent"])); + $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']); + + $attributes = array( + "ref" => $parent_item, + "type" => "text/html", + "href" => App::get_baseurl()."/display/".$parent[0]["guid"]); + xml::add_element($doc, $entry, "thr:in-reply-to", "", $attributes); + + $attributes = array( + "rel" => "related", + "href" => App::get_baseurl()."/display/".$parent[0]["guid"]); + xml::add_element($doc, $entry, "link", "", $attributes); + + $mentioned[$parent[0]["author-link"]] = $parent[0]["author-link"]; + $mentioned[$parent[0]["owner-link"]] = $parent[0]["owner-link"]; + + $thrparent = q("SELECT `guid`, `author-link`, `owner-link` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", + intval($owner["uid"]), + dbesc($parent_item)); + if ($thrparent) { + $mentioned[$thrparent[0]["author-link"]] = $thrparent[0]["author-link"]; + $mentioned[$thrparent[0]["owner-link"]] = $thrparent[0]["owner-link"]; + } + } + + if (intval($item["parent"]) > 0) { + $conversation = App::get_baseurl()."/display/".$owner["nick"]."/".$item["parent"]; + xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:conversation", "href" => $conversation)); + xml::add_element($doc, $entry, "ostatus:conversation", $conversation); + } + + $tags = item_getfeedtags($item); + + if(count($tags)) + foreach($tags as $t) + if ($t[0] == "@") + $mentioned[$t[1]] = $t[1]; + + // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS) + $newmentions = array(); + foreach ($mentioned AS $mention) { + $newmentions[str_replace("http://", "https://", $mention)] = str_replace("http://", "https://", $mention); + $newmentions[str_replace("https://", "http://", $mention)] = str_replace("https://", "http://", $mention); + } + $mentioned = $newmentions; + + foreach ($mentioned AS $mention) { + $r = q("SELECT `forum`, `prv` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'", + intval($owner["uid"]), + dbesc(normalise_link($mention))); + if ($r[0]["forum"] OR $r[0]["prv"]) + xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned", + "ostatus:object-type" => ACTIVITY_OBJ_GROUP, + "href" => $mention)); + else + xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned", + "ostatus:object-type" => ACTIVITY_OBJ_PERSON, + "href" => $mention)); + } + + if (!$item["private"]) { + xml::add_element($doc, $entry, "link", "", array("rel" => "ostatus:attention", + "href" => "http://activityschema.org/collection/public")); + xml::add_element($doc, $entry, "link", "", array("rel" => "mentioned", + "ostatus:object-type" => "http://activitystrea.ms/schema/1.0/collection", + "href" => "http://activityschema.org/collection/public")); + } + + if(count($tags)) + foreach($tags as $t) + if ($t[0] != "@") + xml::add_element($doc, $entry, "category", "", array("term" => $t[2])); + + self::get_attachment($doc, $entry, $item); + + if ($complete AND ($item["id"] > 0)) { + $app = $item["app"]; + if ($app == "") + $app = "web"; + + $attributes = array("local_id" => $item["id"], "source" => $app); + + if (isset($parent["id"])) + $attributes["repeat_of"] = $parent["id"]; + + if ($item["coord"] != "") + xml::add_element($doc, $entry, "georss:point", $item["coord"]); + + xml::add_element($doc, $entry, "statusnet:notice_info", "", $attributes); + } + } + + /** + * @brief Creates the XML feed for a given nickname + * + * @param app $a The application class + * @param string $owner_nick Nickname of the feed owner + * @param string $last_update Date of the last update + * + * @return string XML feed + */ + public static function feed(App $a, $owner_nick, $last_update) { + + $r = q("SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags` + FROM `contact` INNER JOIN `user` ON `user`.`uid` = `contact`.`uid` + WHERE `contact`.`self` AND `user`.`nickname` = '%s' LIMIT 1", + dbesc($owner_nick)); if (!$r) return; - $profile = $r[0]; + $owner = $r[0]; - $author = ostatus_add_author($doc, $owner, $profile); - $entry->appendChild($author); + if(!strlen($last_update)) + $last_update = 'now -30 days'; - $title = sprintf("New comment by %s", $owner["nick"]); - } + $check_date = datetime_convert('UTC','UTC',$last_update,'Y-m-d H:i:s'); + $authorid = get_contact($owner["url"], 0); - // To use the object-type "bookmark" we have to implement these elements: - // - // http://activitystrea.ms/schema/1.0/bookmark - // Historic Rocket Landing - //

Nur ein Testbeitrag. - // - // - // - // But: it seems as if it doesn't federate well between the GS servers - // So we just set it to "note" to be sure that it reaches their target systems + $items = q("SELECT `item`.*, `item`.`id` AS `item_id` FROM `item` USE INDEX (`uid_contactid_created`) + STRAIGHT_JOIN `thread` ON `thread`.`iid` = `item`.`parent` + WHERE `item`.`uid` = %d AND `item`.`contact-id` = %d AND + `item`.`author-id` = %d AND `item`.`created` > '%s' AND + NOT `item`.`deleted` AND NOT `item`.`private` AND + `thread`.`network` IN ('%s', '%s') + ORDER BY `item`.`created` DESC LIMIT 300", + intval($owner["uid"]), intval($owner["id"]), + intval($authorid), dbesc($check_date), + dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN)); - xml_add_element($doc, $entry, "activity:object-type", ACTIVITY_OBJ_NOTE); - xml_add_element($doc, $entry, "id", $item["uri"]); - xml_add_element($doc, $entry, "title", $title); +/* 2016-10-23: The old query will be kept until we are sure that the query above is a good and fast replacement - if($item['allow_cid'] || $item['allow_gid'] || $item['deny_cid'] || $item['deny_gid']) - $body = fix_private_photos($item['body'],$owner['uid'],$item, 0); - else - $body = $item['body']; + $items = q("SELECT `item`.*, `item`.`id` AS `item_id` FROM `item` + STRAIGHT_JOIN `thread` ON `thread`.`iid` = `item`.`parent` + LEFT JOIN `item` AS `thritem` ON `thritem`.`uri`=`item`.`thr-parent` AND `thritem`.`uid`=`item`.`uid` + WHERE `item`.`uid` = %d AND `item`.`received` > '%s' AND NOT `item`.`private` AND NOT `item`.`deleted` + AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' + AND ((`item`.`wall` AND (`item`.`parent` = `item`.`id`)) + OR (`item`.`network` = '%s' AND ((`thread`.`network` IN ('%s', '%s')) OR (`thritem`.`network` IN ('%s', '%s')))) AND `thread`.`mention`) + AND ((`item`.`owner-link` IN ('%s', '%s') AND (`item`.`parent` = `item`.`id`)) + OR (`item`.`author-link` IN ('%s', '%s'))) + ORDER BY `item`.`id` DESC + LIMIT 0, 300", + intval($owner["uid"]), dbesc($check_date), dbesc(NETWORK_DFRN), + //dbesc(NETWORK_OSTATUS), dbesc(NETWORK_OSTATUS), + //dbesc(NETWORK_OSTATUS), dbesc(NETWORK_OSTATUS), + dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN), + dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DFRN), + dbesc($owner["nurl"]), dbesc(str_replace("http://", "https://", $owner["nurl"])), + dbesc($owner["nurl"]), dbesc(str_replace("http://", "https://", $owner["nurl"])) + ); +*/ + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->formatOutput = true; - $body = ostatus_format_picture_post($body); + $root = self::add_header($doc, $owner); - if ($item['title'] != "") - $body = "[b]".$item['title']."[/b]\n\n".$body; - - //$body = bb_remove_share_information($body); - $body = bbcode($body, false, false, 7); - - xml_add_element($doc, $entry, "content", $body, array("type" => "html")); - - xml_add_element($doc, $entry, "link", "", array("rel" => "alternate", "type" => "text/html", - "href" => $a->get_baseurl()."/display/".$item["guid"])); - - xml_add_element($doc, $entry, "status_net", "", array("notice_id" => $item["id"])); - xml_add_element($doc, $entry, "activity:verb", construct_verb($item)); - xml_add_element($doc, $entry, "published", datetime_convert("UTC","UTC",$item["created"]."+00:00",ATOM_TIME)); - xml_add_element($doc, $entry, "updated", datetime_convert("UTC","UTC",$item["edited"]."+00:00",ATOM_TIME)); - - $mentioned = array(); - - if (($item['parent'] != $item['id']) || ($item['parent-uri'] !== $item['uri']) || (($item['thr-parent'] !== '') && ($item['thr-parent'] !== $item['uri']))) { - $parent = q("SELECT `guid` FROM `item` WHERE `id` = %d", intval($item["parent"])); - $parent_item = (($item['thr-parent']) ? $item['thr-parent'] : $item['parent-uri']); - - $attributes = array( - "ref" => $parent_item, - "type" => "text/html", - "href" => $a->get_baseurl()."/display/".$parent[0]["guid"]); - xml_add_element($doc, $entry, "thr:in-reply-to", "", $attributes); - - $attributes = array( - "rel" => "related", - "href" => $a->get_baseurl()."/display/".$parent[0]["guid"]); - xml_add_element($doc, $entry, "link", "", $attributes); - - $mentioned[$parent[0]["author-link"]] = $parent[0]["author-link"]; - $mentioned[$parent[0]["owner-link"]] = $parent[0]["owner-link"]; - - $thrparent = q("SELECT `guid`, `author-link`, `owner-link` FROM `item` WHERE `uid` = %d AND `uri` = '%s'", - intval($owner["uid"]), - dbesc($parent_item)); - if ($thrparent) { - $mentioned[$thrparent[0]["author-link"]] = $thrparent[0]["author-link"]; - $mentioned[$thrparent[0]["owner-link"]] = $thrparent[0]["owner-link"]; + foreach ($items AS $item) { + $entry = self::entry($doc, $item, $owner); + $root->appendChild($entry); } + + return(trim($doc->saveXML())); } - xml_add_element($doc, $entry, "link", "", array("rel" => "ostatus:conversation", - "href" => $a->get_baseurl()."/display/".$owner["nick"]."/".$item["parent"])); - xml_add_element($doc, $entry, "ostatus:conversation", $a->get_baseurl()."/display/".$owner["nick"]."/".$item["parent"]); + /** + * @brief Creates the XML for a salmon message + * + * @param array $item Data of the item that is to be posted + * @param array $owner Contact data of the poster + * + * @return string XML for the salmon + */ + public static function salmon($item,$owner) { - $tags = item_getfeedtags($item); + $doc = new DOMDocument('1.0', 'utf-8'); + $doc->formatOutput = true; - if(count($tags)) - foreach($tags as $t) - if ($t[0] == "@") - $mentioned[$t[1]] = $t[1]; + $entry = self::entry($doc, $item, $owner, true); - // Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS) - $newmentions = array(); - foreach ($mentioned AS $mention) { - $newmentions[str_replace("http://", "https://", $mention)] = str_replace("http://", "https://", $mention); - $newmentions[str_replace("https://", "http://", $mention)] = str_replace("https://", "http://", $mention); + $doc->appendChild($entry); + + return(trim($doc->saveXML())); } - $mentioned = $newmentions; - - foreach ($mentioned AS $mention) { - $r = q("SELECT `forum`, `prv` FROM `contact` WHERE `uid` = %d AND `nurl` = '%s'", - intval($owner["uid"]), - dbesc(normalise_link($mention))); - if ($r[0]["forum"] OR $r[0]["prv"]) - xml_add_element($doc, $entry, "link", "", array("rel" => "mentioned", - "ostatus:object-type" => ACTIVITY_OBJ_GROUP, - "href" => $mention)); - else - xml_add_element($doc, $entry, "link", "", array("rel" => "mentioned", - "ostatus:object-type" => ACTIVITY_OBJ_PERSON, - "href" => $mention)); - } - - if (!$item["private"]) - xml_add_element($doc, $entry, "link", "", array("rel" => "mentioned", - "ostatus:object-type" => "http://activitystrea.ms/schema/1.0/collection", - "href" => "http://activityschema.org/collection/public")); - - if(count($tags)) - foreach($tags as $t) - if ($t[0] != "@") - xml_add_element($doc, $entry, "category", "", array("term" => $t[2])); - - ostatus_get_attachment($doc, $entry, $item); - - // To-Do: - // The API call has yet to be implemented - //$attributes = array("href" => $a->get_baseurl()."/api/statuses/show/".$item["id"].".atom", - // "rel" => "self", "type" => "application/atom+xml"); - //xml_add_element($doc, $entry, "link", "", $attributes); - - //$attributes = array("href" => $a->get_baseurl()."/api/statuses/show/".$item["id"].".atom", - // "rel" => "edit", "type" => "application/atom+xml"); - //xml_add_element($doc, $entry, "link", "", $attributes); - - $app = $item["app"]; - if ($app == "") - $app = "web"; - - xml_add_element($doc, $entry, "statusnet:notice_info", "", array("local_id" => $item["id"], "source" => $app)); - - return $entry; -} - -function ostatus_feed(&$a, $owner_nick, $last_update) { - - $r = q("SELECT `contact`.*, `user`.`nickname`, `user`.`timezone`, `user`.`page-flags` - FROM `contact` INNER JOIN `user` ON `user`.`uid` = `contact`.`uid` - WHERE `contact`.`self` AND `user`.`nickname` = '%s' LIMIT 1", - dbesc($owner_nick)); - if (!$r) - return; - - $owner = $r[0]; - - if(!strlen($last_update)) - $last_update = 'now -30 days'; - - $check_date = datetime_convert('UTC','UTC',$last_update,'Y-m-d H:i:s'); - - $items = q("SELECT STRAIGHT_JOIN `item`.*, `item`.`id` AS `item_id` FROM `item` - INNER JOIN `thread` ON `thread`.`iid` = `item`.`parent` - LEFT JOIN `item` AS `thritem` ON `thritem`.`uri`=`item`.`thr-parent` AND `thritem`.`uid`=`item`.`uid` - WHERE `item`.`uid` = %d AND `item`.`received` > '%s' AND NOT `item`.`private` AND NOT `item`.`deleted` - AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' - AND ((`item`.`wall` AND (`item`.`parent` = `item`.`id`)) - OR (`item`.`network` = '%s' AND ((`thread`.`network`='%s') OR (`thritem`.`network` = '%s'))) AND `thread`.`mention`) - AND (`item`.`owner-link` IN ('%s', '%s')) - ORDER BY `item`.`received` DESC - LIMIT 0, 300", - intval($owner["uid"]), dbesc($check_date), - dbesc(NETWORK_DFRN), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_OSTATUS), - dbesc($owner["nurl"]), dbesc(str_replace("http://", "https://", $owner["nurl"])) - ); - - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->formatOutput = true; - - $root = ostatus_add_header($doc, $owner); - - foreach ($items AS $item) { - $entry = ostatus_entry($doc, $item, $owner); - $root->appendChild($entry); - } - - return(trim($doc->saveXML())); -} - -function ostatus_salmon($item,$owner) { - - $doc = new DOMDocument('1.0', 'utf-8'); - $doc->formatOutput = true; - - $entry = ostatus_entry($doc, $item, $owner, true); - - $doc->appendChild($entry); - - return(trim($doc->saveXML())); } ?> diff --git a/include/pgettext.php b/include/pgettext.php index f72cbb08a7..335869eda2 100644 --- a/include/pgettext.php +++ b/include/pgettext.php @@ -1,13 +1,7 @@ 0.8 - $langs = array_combine($lang_parse[1], $lang_parse[4]); - - // set default to 1 for any without q factor - foreach ($langs as $lang => $val) { - if ($val === '') $langs[$lang] = 1; - } - - // sort list based on value - arsort($langs, SORT_NUMERIC); - } - } - - if(isset($langs) && count($langs)) { - foreach ($langs as $lang => $v) { - if(file_exists("view/$lang") && is_dir("view/$lang")) { - $preferred = $lang; - break; + // go through the list of prefered languages and add a generic language + // for sub-linguas (e.g. de-ch will add de) if not already in array + for ($i=0; $i3 ) { + $dashpos = strpos($lang_parse[1][$i], '-'); + if (! in_array(substr($lang_parse[1][$i], 0, $dashpos), $lang_list ) ) { + $lang_list[] = strtolower(substr($lang_parse[1][$i], 0, $dashpos)); + } + } } } } - if(isset($preferred)) + // check if we have translations for the preferred languages and pick the 1st that has + for ($i=0; $iconfig['system']['language'])) ? $a->config['system']['language'] : 'en'); + // in case none matches, get the system wide configured language, or fall back to English + return Config::get('system', 'language', 'en'); }} @@ -98,7 +100,7 @@ if(! function_exists('load_translation_table')) { * @param string $lang language code to load */ function load_translation_table($lang) { - global $a; + $a = get_app(); $a->strings = array(); // load enabled plugins strings @@ -112,8 +114,8 @@ function load_translation_table($lang) { } } - if(file_exists("view/$lang/strings.php")) { - include("view/$lang/strings.php"); + if(file_exists("view/lang/$lang/strings.php")) { + include("view/lang/$lang/strings.php"); } }} @@ -162,25 +164,31 @@ function string_plural_select_default($n) { }} -/** - * Return installed languages as associative array - * [ - * lang => lang, - * ... - * ] - */ -function get_avaiable_languages() { - $lang_choices = array(); - $langs = glob('view/*/strings.php'); /**/ - if(is_array($langs) && count($langs)) { - if(! in_array('view/en/strings.php',$langs)) - $langs[] = 'view/en/'; - asort($langs); - foreach($langs as $l) { - $t = explode("/",$l); - $lang_choices[$t[1]] = $t[1]; +/** + * @brief Return installed languages codes as associative array + * + * Scans the view/lang directory for the existence of "strings.php" files, and + * returns an alphabetical list of their folder names (@-char language codes). + * Adds the english language if it's missing from the list. + * + * Ex: array('de' => 'de', 'en' => 'en', 'fr' => 'fr', ...) + * + * @return array + */ +function get_available_languages() { + $langs = array(); + $strings_file_paths = glob('view/lang/*/strings.php'); + + if (is_array($strings_file_paths) && count($strings_file_paths)) { + if (!in_array('view/lang/en/strings.php', $strings_file_paths)) { + $strings_file_paths[] = 'view/lang/en/strings.php'; + } + asort($strings_file_paths); + foreach($strings_file_paths as $strings_file_path) { + $path_array = explode('/', $strings_file_path); + $langs[$path_array[2]] = $path_array[2]; } } - return $lang_choices; + return $langs; } diff --git a/include/photos.php b/include/photos.php index 93a565b511..9d8d3309c2 100644 --- a/include/photos.php +++ b/include/photos.php @@ -4,6 +4,9 @@ * @brief Functions related to photo handling. */ +use \Friendica\Core\Config; +use \Friendica\Core\PConfig; + function getGps($exifCoord, $hemi) { $degrees = count($exifCoord) > 0 ? gps2Num($exifCoord[0]) : 0; $minutes = count($exifCoord) > 1 ? gps2Num($exifCoord[1]) : 0; @@ -25,3 +28,46 @@ function gps2Num($coordPart) { return floatval($parts[0]) / floatval($parts[1]); } + +/** + * @brief Fetch the photo albums that are available for a viewer + * + * The query in this function is cost intensive, so it is cached. + * + * @param int $uid User id of the photos + * @param bool $update Update the cache + * + * @return array Returns array of the photo albums + */ +function photo_albums($uid, $update = false) { + $sql_extra = permissions_sql($uid); + + $key = "photo_albums:".$uid.":".local_user().":".remote_user(); + $albums = Cache::get($key); + if (is_null($albums) OR $update) { + if (!Config::get('system', 'no_count', false)) { + /// @todo This query needs to be renewed. It is really slow + // At this time we just store the data in the cache + $albums = qu("SELECT COUNT(DISTINCT `resource-id`) AS `total`, `album` + FROM `photo` + WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra + GROUP BY `album` ORDER BY `created` DESC", + intval($uid), + dbesc('Contact Photos'), + dbesc(t('Contact Photos')) + ); + } else { + // This query doesn't do the count and is much faster + $albums = qu("SELECT DISTINCT(`album`), '' AS `total` + FROM `photo` + WHERE `uid` = %d AND `album` != '%s' AND `album` != '%s' $sql_extra + GROUP BY `album` ORDER BY `created` DESC", + intval($uid), + dbesc('Contact Photos'), + dbesc(t('Contact Photos')) + ); + } + Cache::set($key, $albums, CACHE_DAY); + } + return $albums; +} diff --git a/include/plaintext.php b/include/plaintext.php index 204feb137f..6ab4ec77d6 100644 --- a/include/plaintext.php +++ b/include/plaintext.php @@ -1,4 +1,184 @@ Message type ("link", "video", "photo") + * 'text' -> Text before the shared message + * 'after' -> Text after the shared message + * 'image' -> Preview image of the message + * 'url' -> Url to the attached message + * 'title' -> Title of the attachment + * 'description' -> Description of the attachment + */ +function get_old_attachment_data($body) { + + $post = array(); + + // Simplify image codes + $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); + + if (preg_match_all("(\[class=(.*?)\](.*?)\[\/class\])ism",$body, $attached, PREG_SET_ORDER)) { + foreach ($attached AS $data) { + if (!in_array($data[1], array("type-link", "type-video", "type-photo"))) + continue; + + $post["type"] = substr($data[1], 5); + + $pos = strpos($body, $data[0]); + if ($pos > 0) { + $post["text"] = trim(substr($body, 0, $pos)); + $post["after"] = trim(substr($body, $pos + strlen($data[0]))); + } else + $post["text"] = trim(str_replace($data[0], "", $body)); + + $attacheddata = $data[2]; + + $URLSearchString = "^\[\]"; + + if (preg_match("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $attacheddata, $matches)) { + + $picturedata = get_photo_info($matches[1]); + + if (($picturedata[0] >= 500) AND ($picturedata[0] >= $picturedata[1])) + $post["image"] = $matches[1]; + else + $post["preview"] = $matches[1]; + } + + if (preg_match("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", $attacheddata, $matches)) { + $post["url"] = $matches[1]; + $post["title"] = $matches[2]; + } + if (($post["url"] == "") AND (in_array($post["type"], array("link", "video"))) + AND preg_match("/\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", $attacheddata, $matches)) { + $post["url"] = $matches[1]; + } + + // Search for description + if (preg_match("/\[quote\](.*?)\[\/quote\]/ism", $attacheddata, $matches)) + $post["description"] = $matches[1]; + + } + } + + return $post; +} + +/** + * @brief Fetches attachment data that were generated with the "attachment" element + * + * @param string $body Message body + * @return array + * 'type' -> Message type ("link", "video", "photo") + * 'text' -> Text before the shared message + * 'after' -> Text after the shared message + * 'image' -> Preview image of the message + * 'url' -> Url to the attached message + * 'title' -> Title of the attachment + * 'description' -> Description of the attachment + */ +function get_attachment_data($body) { + + $data = array(); + + if (!preg_match("/(.*)\[attachment(.*?)\](.*?)\[\/attachment\](.*)/ism", $body, $match)) + return get_old_attachment_data($body); + + $attributes = $match[2]; + + $data["text"] = trim($match[1]); + + $type = ""; + preg_match("/type='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $type = strtolower($matches[1]); + + preg_match('/type="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $type = strtolower($matches[1]); + + if ($type == "") + return(array()); + + if (!in_array($type, array("link", "audio", "photo", "video"))) + return(array()); + + if ($type != "") + $data["type"] = $type; + + $url = ""; + preg_match("/url='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $url = $matches[1]; + + preg_match('/url="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $url = $matches[1]; + + if ($url != "") + $data["url"] = html_entity_decode($url, ENT_QUOTES, 'UTF-8'); + + $title = ""; + preg_match("/title='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $title = $matches[1]; + + preg_match('/title="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $title = $matches[1]; + + if ($title != "") { + $title = bbcode(html_entity_decode($title, ENT_QUOTES, 'UTF-8'), false, false, true); + $title = html_entity_decode($title, ENT_QUOTES, 'UTF-8'); + $title = str_replace(array("[", "]"), array("[", "]"), $title); + $data["title"] = $title; + } + + $image = ""; + preg_match("/image='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $image = $matches[1]; + + preg_match('/image="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $image = $matches[1]; + + if ($image != "") + $data["image"] = html_entity_decode($image, ENT_QUOTES, 'UTF-8'); + + $preview = ""; + preg_match("/preview='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $preview = $matches[1]; + + preg_match('/preview="(.*?)"/ism', $attributes, $matches); + if ($matches[1] != "") + $preview = $matches[1]; + + if ($preview != "") + $data["preview"] = html_entity_decode($preview, ENT_QUOTES, 'UTF-8'); + + $data["description"] = trim($match[3]); + + $data["after"] = trim($match[4]); + + return($data); +} + function get_attached_data($body) { /* - text: @@ -10,49 +190,22 @@ function get_attached_data($body) { - (thumbnail) */ - // Simplify image codes - $body = preg_replace("/\[img\=([0-9]*)x([0-9]*)\](.*?)\[\/img\]/ism", '[img]$3[/img]', $body); - - $post = array(); - - if (preg_match_all("(\[class=(.*?)\](.*?)\[\/class\])ism",$body, $attached, PREG_SET_ORDER)) { - foreach ($attached AS $data) { - if (!in_array($data[1], array("type-link", "type-video", "type-photo"))) - continue; - - $post["type"] = substr($data[1], 5); - - $post["text"] = trim(str_replace($data[0], "", $body)); - - $attacheddata = $data[2]; - - $URLSearchString = "^\[\]"; - - if (preg_match("/\[img\]([$URLSearchString]*)\[\/img\]/ism", $attacheddata, $matches)) - $post["image"] = $matches[1]; - - if (preg_match("/\[bookmark\=([$URLSearchString]*)\](.*?)\[\/bookmark\]/ism", $attacheddata, $matches)) { - $post["url"] = $matches[1]; - $post["title"] = $matches[2]; - } - - // Search for description - if (preg_match("/\[quote\](.*?)\[\/quote\]/ism", $attacheddata, $matches)) - $post["description"] = $matches[1]; - - } - } + $post = get_attachment_data($body); // if nothing is found, it maybe having an image. if (!isset($post["type"])) { - require_once("mod/parse_url.php"); - require_once("include/Photo.php"); - $URLSearchString = "^\[\]"; if (preg_match_all("(\[url=([$URLSearchString]*)\]\s*\[img\]([$URLSearchString]*)\[\/img\]\s*\[\/url\])ism", $body, $pictures, PREG_SET_ORDER)) { if (count($pictures) == 1) { // Checking, if the link goes to a picture - $data = parseurl_getsiteinfo_cached($pictures[0][1], true); + $data = ParseUrl::getSiteinfoCached($pictures[0][1], true); + + // Workaround: + // Sometimes photo posts to the own album are not detected at the start. + // So we seem to cannot use the cache for these cases. That's strange. + if (($data["type"] != "photo") AND strstr($pictures[0][1], "/photos/")) + $data = ParseUrl::getSiteinfo($pictures[0][1], true); + if ($data["type"] == "photo") { $post["type"] = "photo"; if (isset($data["images"][0])) { @@ -90,13 +243,20 @@ function get_attached_data($body) { $post["text"] = $body; } } + + if (preg_match_all("(\[url\]([$URLSearchString]*)\[\/url\])ism", $body, $links, PREG_SET_ORDER)) { + if (count($links) == 1) { + $post["type"] = "text"; + $post["url"] = $links[0][1]; + $post["text"] = $body; + } + } if (!isset($post["type"])) { $post["type"] = "text"; $post["text"] = trim($body); } } elseif (isset($post["url"]) AND ($post["type"] == "video")) { - require_once("mod/parse_url.php"); - $data = parseurl_getsiteinfo_cached($post["url"], true); + $data = ParseUrl::getSiteinfoCached($post["url"], true); if (isset($data["images"][0])) $post["image"] = $data["images"][0]["src"]; @@ -106,39 +266,98 @@ function get_attached_data($body) { } function shortenmsg($msg, $limit, $twitter = false) { - // To-Do: - // For Twitter URLs aren't shortened, but they have to be calculated as if. + /// @TODO + /// For Twitter URLs aren't shortened, but they have to be calculated as if. $lines = explode("\n", $msg); $msg = ""; $recycle = html_entity_decode("♲ ", ENT_QUOTES, 'UTF-8'); + $ellipsis = html_entity_decode("…", ENT_QUOTES, 'UTF-8'); foreach ($lines AS $row=>$line) { if (iconv_strlen(trim($msg."\n".$line), "UTF-8") <= $limit) $msg = trim($msg."\n".$line); // Is the new message empty by now or is it a reshared message? elseif (($msg == "") OR (($row == 1) AND (substr($msg, 0, 4) == $recycle))) - $msg = iconv_substr(iconv_substr(trim($msg."\n".$line), 0, $limit, "UTF-8"), 0, -3, "UTF-8")."..."; + $msg = iconv_substr(iconv_substr(trim($msg."\n".$line), 0, $limit, "UTF-8"), 0, -3, "UTF-8").$ellipsis; else break; } return($msg); } -function plaintext($a, $b, $limit = 0, $includedlinks = false, $htmlmode = 2) { - require_once("include/bbcode.php"); - require_once("include/html2plain.php"); - require_once("include/network.php"); +/** + * @brief Convert a message into plaintext for connectors to other networks + * + * @param App $a The application class + * @param array $b The message array that is about to be posted + * @param int $limit The maximum number of characters when posting to that network + * @param bool $includedlinks Has an attached link to be included into the message? + * @param int $htmlmode This triggers the behaviour of the bbcode conversion + * @param string $target_network Name of the network where the post should go to. + * + * @return string The converted message + */ +function plaintext(App $a, $b, $limit = 0, $includedlinks = false, $htmlmode = 2, $target_network = "") { + + // Remove the hash tags + $URLSearchString = "^\[\]"; + $body = preg_replace("/([#@])\[url\=([$URLSearchString]*)\](.*?)\[\/url\]/ism", '$1$3', $b["body"]); + + // Add an URL element if the text contains a raw link + $body = preg_replace("/([^\]\='".'"'."]|^)(https?\:\/\/[a-zA-Z0-9\:\/\-\?\&\;\.\=\_\~\#\%\$\!\+\,]+)/ism", '$1[url]$2[/url]', $body); + + // Remove the abstract + $body = remove_abstract($body); // At first look at data that is attached via "type-..." stuff // This will hopefully replaced with a dedicated bbcode later - $post = get_attached_data($b["body"]); + //$post = get_attached_data($b["body"]); + $post = get_attached_data($body); if (($b["title"] != "") AND ($post["text"] != "")) $post["text"] = trim($b["title"]."\n\n".$post["text"]); elseif ($b["title"] != "") $post["text"] = trim($b["title"]); - $html = bbcode($post["text"], false, false, $htmlmode); + $abstract = ""; + + // Fetch the abstract from the given target network + if ($target_network != "") { + $default_abstract = fetch_abstract($b["body"]); + $abstract = fetch_abstract($b["body"], $target_network); + + // If we post to a network with no limit we only fetch + // an abstract exactly for this network + if (($limit == 0) AND ($abstract == $default_abstract)) + $abstract = ""; + + } else // Try to guess the correct target network + switch ($htmlmode) { + case 8: + $abstract = fetch_abstract($b["body"], NETWORK_TWITTER); + break; + case 7: + $abstract = fetch_abstract($b["body"], NETWORK_STATUSNET); + break; + case 6: + $abstract = fetch_abstract($b["body"], NETWORK_APPNET); + break; + default: // We don't know the exact target. + // We fetch an abstract since there is a posting limit. + if ($limit > 0) + $abstract = fetch_abstract($b["body"]); + } + + if ($abstract != "") { + $post["text"] = $abstract; + + if ($post["type"] == "text") { + $post["type"] = "link"; + $post["url"] = $b["plink"]; + } + } + + $html = bbcode($post["text"].$post["after"], false, false, $htmlmode); $msg = html2plain($html, 0, true); $msg = trim(html_entity_decode($msg,ENT_QUOTES,'UTF-8')); @@ -146,6 +365,8 @@ function plaintext($a, $b, $limit = 0, $includedlinks = false, $htmlmode = 2) { if ($includedlinks) { if ($post["type"] == "link") $link = $post["url"]; + elseif ($post["type"] == "text") + $link = $post["url"]; elseif ($post["type"] == "video") $link = $post["url"]; elseif ($post["type"] == "photo") @@ -161,8 +382,22 @@ function plaintext($a, $b, $limit = 0, $includedlinks = false, $htmlmode = 2) { // But: if the link is beyond the limit, then it has to be added. if (($link != "") AND strstr($msg, $link)) { $pos = strpos($msg, $link); - if (($limit == 0) OR ($pos < $limit)) + + // Will the text be shortened in the link? + // Or is the link the last item in the post? + if (($limit > 0) AND ($pos < $limit) AND (($pos + 23 > $limit) OR ($pos + strlen($link) == strlen($msg)))) + $msg = trim(str_replace($link, "", $msg)); + elseif (($limit == 0) OR ($pos < $limit)) { + // The limit has to be increased since it will be shortened - but not now + // Only do it with Twitter (htmlmode = 8) + if (($limit > 0) AND (strlen($link) > 23) AND ($htmlmode == 8)) + $limit = $limit - 23 + strlen($link); + $link = ""; + + if ($post["type"] == "text") + unset($post["url"]); + } } } @@ -178,7 +413,9 @@ function plaintext($a, $b, $limit = 0, $includedlinks = false, $htmlmode = 2) { if (iconv_strlen($msg, "UTF-8") > $limit) { - if (!isset($post["url"])) { + if (($post["type"] == "text") AND isset($post["url"])) + $post["url"] = $b["plink"]; + elseif (!isset($post["url"])) { $limit = $limit - 23; $post["url"] = $b["plink"]; } elseif (strpos($b["body"], "[share") !== false) diff --git a/include/plugin.php b/include/plugin.php index 965b823b02..83f6f1ab95 100644 --- a/include/plugin.php +++ b/include/plugin.php @@ -1,6 +1,6 @@ 0)); + return ((dbm::is_result($r)) && (count($r) > 0)); } @@ -150,7 +150,7 @@ function register_hook($hook,$file,$function,$priority=0) { dbesc($file), dbesc($function) ); - if(count($r)) + if (dbm::is_result($r)) return true; $r = q("INSERT INTO `hook` (`hook`, `file`, `function`, `priority`) VALUES ( '%s', '%s', '%s', '%s' ) ", @@ -187,8 +187,9 @@ function load_hooks() { $a = get_app(); $a->hooks = array(); $r = q("SELECT * FROM `hook` WHERE 1 ORDER BY `priority` DESC, `file`"); - if(count($r)) { - foreach($r as $rr) { + + if (dbm::is_result($r)) { + foreach ($r as $rr) { if(! array_key_exists($rr['hook'],$a->hooks)) $a->hooks[$rr['hook']] = array(); $a->hooks[$rr['hook']][] = array($rr['file'],$rr['function']); @@ -205,37 +206,41 @@ function load_hooks() { * @param string $name of the hook to call * @param string|array &$data to transmit to the callback handler */ -if(! function_exists('call_hooks')) { function call_hooks($name, &$data = null) { $stamp1 = microtime(true); $a = get_app(); - #logger($name, LOGGER_ALL); + if (is_array($a->hooks) && array_key_exists($name, $a->hooks)) + foreach ($a->hooks[$name] as $hook) + call_single_hook($a, $name, $hook, $data); +} - if((is_array($a->hooks)) && (array_key_exists($name,$a->hooks))) { - foreach($a->hooks[$name] as $hook) { - // Don't run a theme's hook if the user isn't using the theme - if(strpos($hook[0], 'view/theme/') !== false && strpos($hook[0], 'view/theme/'.current_theme()) === false) - continue; +/** + * @brief Calls a single hook. + * + * @param string $name of the hook to call + * @param array $hook Hook data + * @param string|array &$data to transmit to the callback handler + */ +function call_single_hook($a, $name, $hook, &$data = null) { + // Don't run a theme's hook if the user isn't using the theme + if (strpos($hook[0], 'view/theme/') !== false && strpos($hook[0], 'view/theme/'.current_theme()) === false) + return; - @include_once($hook[0]); - if(function_exists($hook[1])) { - $func = $hook[1]; - //logger($name." => ".$hook[0].":".$func."()", LOGGER_DEBUG); - $func($a,$data); - } - else { - // remove orphan hooks - q("DELETE FROM `hook` WHERE `hook` = '%s' AND `file` = '%s' AND `function` = '%s'", - dbesc($name), - dbesc($hook[0]), - dbesc($hook[1]) - ); - } - } + @include_once($hook[0]); + if (function_exists($hook[1])) { + $func = $hook[1]; + $func($a, $data); + } else { + // remove orphan hooks + q("DELETE FROM `hook` WHERE `hook` = '%s' AND `file` = '%s' AND `function` = '%s'", + dbesc($name), + dbesc($hook[0]), + dbesc($hook[1]) + ); } -}} +} //check if an app_menu hook exist for plugin $name. //Return true if the plugin is an app @@ -406,13 +411,13 @@ function get_theme_info($theme){ * @return string */ function get_theme_screenshot($theme) { - $a = get_app(); $exts = array('.png','.jpg'); foreach($exts as $ext) { - if(file_exists('view/theme/' . $theme . '/screenshot' . $ext)) - return($a->get_baseurl() . '/view/theme/' . $theme . '/screenshot' . $ext); + if (file_exists('view/theme/' . $theme . '/screenshot' . $ext)) { + return(App::get_baseurl() . '/view/theme/' . $theme . '/screenshot' . $ext); + } } - return($a->get_baseurl() . '/images/blank.png'); + return(App::get_baseurl() . '/images/blank.png'); } // install and uninstall theme @@ -420,8 +425,8 @@ if (! function_exists('uninstall_theme')){ function uninstall_theme($theme){ logger("Addons: uninstalling theme " . $theme); - @include_once("view/theme/$theme/theme.php"); - if(function_exists("{$theme}_uninstall")) { + include_once("view/theme/$theme/theme.php"); + if (function_exists("{$theme}_uninstall")) { $func = "{$theme}_uninstall"; $func(); } @@ -431,19 +436,19 @@ if (! function_exists('install_theme')){ function install_theme($theme) { // silently fail if theme was removed - if(! file_exists("view/theme/$theme/theme.php")) + if (! file_exists("view/theme/$theme/theme.php")) { return false; + } logger("Addons: installing theme $theme"); - @include_once("view/theme/$theme/theme.php"); + include_once("view/theme/$theme/theme.php"); - if(function_exists("{$theme}_install")) { + if (function_exists("{$theme}_install")) { $func = "{$theme}_install"; $func(); return true; - } - else { + } else { logger("Addons: FAILED installing theme $theme"); return false; } @@ -462,29 +467,33 @@ function install_theme($theme) { function service_class_allows($uid,$property,$usage = false) { - if($uid == local_user()) { + if ($uid == local_user()) { $service_class = $a->user['service_class']; - } - else { + } else { $r = q("SELECT `service_class` FROM `user` WHERE `uid` = %d LIMIT 1", intval($uid) ); - if($r !== false and count($r)) { + if (dbm::is_result($r)) { $service_class = $r[0]['service_class']; } } - if(! x($service_class)) - return true; // everything is allowed + + if (! x($service_class)) { + // everything is allowed + return true; + } $arr = get_config('service_class',$service_class); - if(! is_array($arr) || (! count($arr))) + if (! is_array($arr) || (! count($arr))) { return true; + } - if($usage === false) + if ($usage === false) { return ((x($arr[$property])) ? (bool) $arr['property'] : true); - else { - if(! array_key_exists($property,$arr)) + } else { + if (! array_key_exists($property,$arr)) { return true; + } return (((intval($usage)) < intval($arr[$property])) ? true : false); } } @@ -492,14 +501,13 @@ function service_class_allows($uid,$property,$usage = false) { function service_class_fetch($uid,$property) { - if($uid == local_user()) { + if ($uid == local_user()) { $service_class = $a->user['service_class']; - } - else { + } else { $r = q("SELECT `service_class` FROM `user` WHERE `uid` = %d LIMIT 1", intval($uid) ); - if($r !== false and count($r)) { + if (dbm::is_result($r)) { $service_class = $r[0]['service_class']; } } @@ -534,3 +542,41 @@ function upgrade_bool_message($bbcode = false) { $x = upgrade_link($bbcode); return t('This action is not available under your subscription plan.') . (($x) ? ' ' . $x : '') ; } + +/** + * @brief Get the full path to relevant theme files by filename + * + * This function search in the theme directory (and if not present in global theme directory) + * if there is a directory with the file extension and for a file with the given + * filename. + * + * @param string $file Filename + * @param string $root Full root path + * @return string Path to the file or empty string if the file isn't found + */ +function theme_include($file, $root = '') { + // Make sure $root ends with a slash / if it's not blank + if($root !== '' && $root[strlen($root)-1] !== '/') + $root = $root . '/'; + $theme_info = $a->theme_info; + if(is_array($theme_info) AND array_key_exists('extends',$theme_info)) + $parent = $theme_info['extends']; + else + $parent = 'NOPATH'; + $theme = current_theme(); + $thname = $theme; + $ext = substr($file,strrpos($file,'.')+1); + $paths = array( + "{$root}view/theme/$thname/$ext/$file", + "{$root}view/theme/$parent/$ext/$file", + "{$root}view/$ext/$file", + ); + foreach($paths as $p) { + // strpos() is faster than strstr when checking if one string is in another (http://php.net/manual/en/function.strstr.php) + if(strpos($p,'NOPATH') !== false) + continue; + if(file_exists($p)) + return $p; + } + return ''; +} diff --git a/include/poller.php b/include/poller.php index b1d6099ad3..8be4c1835c 100644 --- a/include/poller.php +++ b/include/poller.php @@ -10,9 +10,11 @@ if (!file_exists("boot.php") AND (sizeof($_SERVER["argv"]) != 0)) { chdir($directory); } +use \Friendica\Core\Config; + require_once("boot.php"); -function poller_run(&$argv, &$argc){ +function poller_run($argv, $argc){ global $a, $db; if(is_null($a)) { @@ -26,97 +28,50 @@ function poller_run(&$argv, &$argc){ unset($db_host, $db_user, $db_pass, $db_data); }; - $load = current_load(); - if($load) { - $maxsysload = intval(get_config('system','maxloadavg')); - if($maxsysload < 1) - $maxsysload = 50; + Config::load(); - if(intval($load) > $maxsysload) { - logger('system: load ' . $load . ' too high. poller deferred to next scheduled run.'); - return; - } + // Quit when in maintenance + if (Config::get('system', 'maintenance', true)) { + return; + } + + $a->start_process(); + + if (poller_max_connections_reached()) { + return; + } + + if ($a->maxload_reached()) { + return; + } + + if(($argc <= 1) OR ($argv[1] != "no_cron")) { + poller_run_cron(); + } + + if ($a->max_processes_reached()) { + return; } // Checking the number of workers - if (poller_too_much_workers(1)) - return; - - if(($argc <= 1) OR ($argv[1] != "no_cron")) { - // Run the cron job that calls all other jobs - proc_run("php","include/cron.php"); - - // Run the cronhooks job separately from cron for being able to use a different timing - proc_run("php","include/cronhooks.php"); - - // Cleaning dead processes - $r = q("SELECT DISTINCT(`pid`) FROM `workerqueue` WHERE `executed` != '0000-00-00 00:00:00'"); - foreach($r AS $pid) - if (!posix_kill($pid["pid"], 0)) - q("UPDATE `workerqueue` SET `executed` = '0000-00-00 00:00:00', `pid` = 0 WHERE `pid` = %d", - intval($pid["pid"])); - else { - // To-Do: Kill long running processes - // But: Update processes (like the database update) mustn't be killed - } - - } else - // Sleep four seconds before checking for running processes again to avoid having too many workers - sleep(4); - - // Checking number of workers - if (poller_too_much_workers(2)) + if (poller_too_much_workers()) { + poller_kill_stale_workers(); return; + } $starttime = time(); - while ($r = q("SELECT * FROM `workerqueue` WHERE `executed` = '0000-00-00 00:00:00' ORDER BY `created` LIMIT 1")) { + while ($r = poller_worker_process()) { // Count active workers and compare them with a maximum value that depends on the load - if (poller_too_much_workers(3)) + if (poller_too_much_workers()) { return; - - q("UPDATE `workerqueue` SET `executed` = '%s', `pid` = %d WHERE `id` = %d AND `executed` = '0000-00-00 00:00:00'", - dbesc(datetime_convert()), - intval(getmypid()), - intval($r[0]["id"])); - - // Assure that there are no tasks executed twice - $id = q("SELECT `id` FROM `workerqueue` WHERE `id` = %d AND `pid` = %d", - intval($r[0]["id"]), - intval(getmypid())); - if (!$id) { - logger("Queue item ".$r[0]["id"]." was executed multiple times - skip this execution", LOGGER_DEBUG); - continue; } - $argv = json_decode($r[0]["parameter"]); - - $argc = count($argv); - - // Check for existance and validity of the include file - $include = $argv[0]; - - if (!validate_include($include)) { - logger("Include file ".$argv[0]." is not valid!"); - q("DELETE FROM `workerqueue` WHERE `id` = %d", intval($r[0]["id"])); - continue; + if (!poller_execute($r[0])) { + return; } - require_once($include); - - $funcname=str_replace(".php", "", basename($argv[0]))."_run"; - - if (function_exists($funcname)) { - logger("Process ".getmypid()." - ID ".$r[0]["id"].": ".$funcname." ".$r[0]["parameter"]); - $funcname($argv, $argc); - - logger("Process ".getmypid()." - ID ".$r[0]["id"].": ".$funcname." - done"); - - q("DELETE FROM `workerqueue` WHERE `id` = %d", intval($r[0]["id"])); - } else - logger("Function ".$funcname." does not exist"); - // Quit the poller once every hour if (time() > ($starttime + 3600)) return; @@ -124,21 +79,330 @@ function poller_run(&$argv, &$argc){ } -function poller_too_much_workers($stage) { +/** + * @brief Execute a worker entry + * + * @param array $queue Workerqueue entry + * + * @return boolean "true" if further processing should be stopped + */ +function poller_execute($queue) { - $queues = get_config("system", "worker_queues"); + $a = get_app(); - if ($queues == 0) - $queues = 4; + $mypid = getmypid(); + + // Quit when in maintenance + if (Config::get('system', 'maintenance', true)) { + return false; + } + + // Constantly check the number of parallel database processes + if ($a->max_processes_reached()) { + return false; + } + + // Constantly check the number of available database connections to let the frontend be accessible at any time + if (poller_max_connections_reached()) { + return false; + } + + $upd = q("UPDATE `workerqueue` SET `executed` = '%s', `pid` = %d WHERE `id` = %d AND `pid` = 0", + dbesc(datetime_convert()), + intval($mypid), + intval($queue["id"])); + + if (!$upd) { + logger("Couldn't update queue entry ".$queue["id"]." - skip this execution", LOGGER_DEBUG); + q("COMMIT"); + return true; + } + + // Assure that there are no tasks executed twice + $id = q("SELECT `pid`, `executed` FROM `workerqueue` WHERE `id` = %d", intval($queue["id"])); + if (!$id) { + logger("Queue item ".$queue["id"]." vanished - skip this execution", LOGGER_DEBUG); + q("COMMIT"); + return true; + } elseif ((strtotime($id[0]["executed"]) <= 0) OR ($id[0]["pid"] == 0)) { + logger("Entry for queue item ".$queue["id"]." wasn't stored - skip this execution", LOGGER_DEBUG); + q("COMMIT"); + return true; + } elseif ($id[0]["pid"] != $mypid) { + logger("Queue item ".$queue["id"]." is to be executed by process ".$id[0]["pid"]." and not by me (".$mypid.") - skip this execution", LOGGER_DEBUG); + q("COMMIT"); + return true; + } + q("COMMIT"); + + $argv = json_decode($queue["parameter"]); + + // Check for existance and validity of the include file + $include = $argv[0]; + + if (!validate_include($include)) { + logger("Include file ".$argv[0]." is not valid!"); + q("DELETE FROM `workerqueue` WHERE `id` = %d", intval($queue["id"])); + return true; + } + + require_once($include); + + $funcname = str_replace(".php", "", basename($argv[0]))."_run"; + + if (function_exists($funcname)) { + + poller_exec_function($queue, $funcname, $argv); + + q("DELETE FROM `workerqueue` WHERE `id` = %d", intval($queue["id"])); + } else { + logger("Function ".$funcname." does not exist"); + } + + return true; +} + +/** + * @brief Execute a function from the queue + * + * @param array $queue Workerqueue entry + * @param string $funcname name of the function + * @param array $argv Array of values to be passed to the function + */ +function poller_exec_function($queue, $funcname, $argv) { + + $a = get_app(); + + $mypid = getmypid(); + + $argc = count($argv); + + logger("Process ".$mypid." - Prio ".$queue["priority"]." - ID ".$queue["id"].": ".$funcname." ".$queue["parameter"]); + + $stamp = (float)microtime(true); + + // We use the callstack here to analyze the performance of executed worker entries. + // For this reason the variables have to be initialized. + if (Config::get("system", "profiler")) { + $a->performance["start"] = microtime(true); + $a->performance["database"] = 0; + $a->performance["database_write"] = 0; + $a->performance["network"] = 0; + $a->performance["file"] = 0; + $a->performance["rendering"] = 0; + $a->performance["parser"] = 0; + $a->performance["marktime"] = 0; + $a->performance["markstart"] = microtime(true); + $a->callstack = array(); + } + + // For better logging create a new process id for every worker call + // But preserve the old one for the worker + $old_process_id = $a->process_id; + $a->process_id = uniqid("wrk", true); + + $funcname($argv, $argc); + + $a->process_id = $old_process_id; + + $duration = number_format(microtime(true) - $stamp, 3); + + logger("Process ".$mypid." - Prio ".$queue["priority"]." - ID ".$queue["id"].": ".$funcname." - done in ".$duration." seconds."); + + // Write down the performance values into the log + if (Config::get("system", "profiler")) { + $duration = microtime(true)-$a->performance["start"]; + + if (Config::get("rendertime", "callstack")) { + if (isset($a->callstack["database"])) { + $o = "\nDatabase Read:\n"; + foreach ($a->callstack["database"] AS $func => $time) { + $time = round($time, 3); + if ($time > 0) + $o .= $func.": ".$time."\n"; + } + } + if (isset($a->callstack["database_write"])) { + $o .= "\nDatabase Write:\n"; + foreach ($a->callstack["database_write"] AS $func => $time) { + $time = round($time, 3); + if ($time > 0) + $o .= $func.": ".$time."\n"; + } + } + if (isset($a->callstack["network"])) { + $o .= "\nNetwork:\n"; + foreach ($a->callstack["network"] AS $func => $time) { + $time = round($time, 3); + if ($time > 0) + $o .= $func.": ".$time."\n"; + } + } + } else { + $o = ''; + } + + logger("ID ".$queue["id"].": ".$funcname.": ".sprintf("DB: %s/%s, Net: %s, I/O: %s, Other: %s, Total: %s".$o, + number_format($a->performance["database"] - $a->performance["database_write"], 2), + number_format($a->performance["database_write"], 2), + number_format($a->performance["network"], 2), + number_format($a->performance["file"], 2), + number_format($duration - ($a->performance["database"] + $a->performance["network"] + $a->performance["file"]), 2), + number_format($duration, 2)), + LOGGER_DEBUG); + } + + $cooldown = Config::get("system", "worker_cooldown", 0); + + if ($cooldown > 0) { + logger("Process ".$mypid." - Prio ".$queue["priority"]." - ID ".$queue["id"].": ".$funcname." - in cooldown for ".$cooldown." seconds"); + sleep($cooldown); + } +} + +/** + * @brief Checks if the number of database connections has reached a critical limit. + * + * @return bool Are more than 3/4 of the maximum connections used? + */ +function poller_max_connections_reached() { + + // Fetch the max value from the config. This is needed when the system cannot detect the correct value by itself. + $max = Config::get("system", "max_connections"); + + // Fetch the percentage level where the poller will get active + $maxlevel = Config::get("system", "max_connections_level", 75); + + if ($max == 0) { + // the maximum number of possible user connections can be a system variable + $r = q("SHOW VARIABLES WHERE `variable_name` = 'max_user_connections'"); + if ($r) + $max = $r[0]["Value"]; + + // Or it can be granted. This overrides the system variable + $r = q("SHOW GRANTS"); + if ($r) + foreach ($r AS $grants) { + $grant = array_pop($grants); + if (stristr($grant, "GRANT USAGE ON")) + if (preg_match("/WITH MAX_USER_CONNECTIONS (\d*)/", $grant, $match)) + $max = $match[1]; + } + } + + // If $max is set we will use the processlist to determine the current number of connections + // The processlist only shows entries of the current user + if ($max != 0) { + $r = q("SHOW PROCESSLIST"); + if (!dbm::is_result($r)) + return false; + + $used = count($r); + + logger("Connection usage (user values): ".$used."/".$max, LOGGER_DEBUG); + + $level = ($used / $max) * 100; + + if ($level >= $maxlevel) { + logger("Maximum level (".$maxlevel."%) of user connections reached: ".$used."/".$max); + return true; + } + } + + // We will now check for the system values. + // This limit could be reached although the user limits are fine. + $r = q("SHOW VARIABLES WHERE `variable_name` = 'max_connections'"); + if (!$r) + return false; + + $max = intval($r[0]["Value"]); + if ($max == 0) + return false; + + $r = q("SHOW STATUS WHERE `variable_name` = 'Threads_connected'"); + if (!$r) + return false; + + $used = intval($r[0]["Value"]); + if ($used == 0) + return false; + + logger("Connection usage (system values): ".$used."/".$max, LOGGER_DEBUG); + + $level = $used / $max * 100; + + if ($level < $maxlevel) + return false; + + logger("Maximum level (".$level."%) of system connections reached: ".$used."/".$max); + return true; +} + +/** + * @brief fix the queue entry if the worker process died + * + */ +function poller_kill_stale_workers() { + $r = q("SELECT `pid`, `executed`, `priority`, `parameter` FROM `workerqueue` WHERE `executed` != '0000-00-00 00:00:00'"); + + if (!dbm::is_result($r)) { + // No processing here needed + return; + } + + foreach($r AS $pid) + if (!posix_kill($pid["pid"], 0)) + q("UPDATE `workerqueue` SET `executed` = '0000-00-00 00:00:00', `pid` = 0 WHERE `pid` = %d", + intval($pid["pid"])); + else { + // Kill long running processes + + // Check if the priority is in a valid range + if (!in_array($pid["priority"], array(PRIORITY_CRITICAL, PRIORITY_HIGH, PRIORITY_MEDIUM, PRIORITY_LOW, PRIORITY_NEGLIGIBLE))) + $pid["priority"] = PRIORITY_MEDIUM; + + // Define the maximum durations + $max_duration_defaults = array(PRIORITY_CRITICAL => 360, PRIORITY_HIGH => 10, PRIORITY_MEDIUM => 60, PRIORITY_LOW => 180, PRIORITY_NEGLIGIBLE => 360); + $max_duration = $max_duration_defaults[$pid["priority"]]; + + $argv = json_decode($pid["parameter"]); + $argv[0] = basename($argv[0]); + + // How long is the process already running? + $duration = (time() - strtotime($pid["executed"])) / 60; + if ($duration > $max_duration) { + logger("Worker process ".$pid["pid"]." (".implode(" ", $argv).") took more than ".$max_duration." minutes. It will be killed now."); + posix_kill($pid["pid"], SIGTERM); + + // We killed the stale process. + // To avoid a blocking situation we reschedule the process at the beginning of the queue. + // Additionally we are lowering the priority. + q("UPDATE `workerqueue` SET `executed` = '0000-00-00 00:00:00', `created` = '%s', + `priority` = %d, `pid` = 0 WHERE `pid` = %d", + dbesc(datetime_convert()), + intval(PRIORITY_NEGLIGIBLE), + intval($pid["pid"])); + } else + logger("Worker process ".$pid["pid"]." (".implode(" ", $argv).") now runs for ".round($duration)." of ".$max_duration." allowed minutes. That's okay.", LOGGER_DEBUG); + } +} + +/** + * @brief Checks if the number of active workers exceeds the given limits + * + * @return bool Are there too much workers running? + */ +function poller_too_much_workers() { + $queues = Config::get("system", "worker_queues", 4); + + $maxqueues = $queues; $active = poller_active_workers(); // Decrease the number of workers at higher load $load = current_load(); if($load) { - $maxsysload = intval(get_config('system','maxloadavg')); - if($maxsysload < 1) - $maxsysload = 50; + $maxsysload = intval(Config::get("system", "maxloadavg", 50)); $maxworkers = $queues; @@ -147,21 +411,250 @@ function poller_too_much_workers($stage) { $slope = $maxworkers / pow($maxsysload, $exponent); $queues = ceil($slope * pow(max(0, $maxsysload - $load), $exponent)); - logger("Current load stage ".$stage.": ".$load." - maximum: ".$maxsysload." - current queues: ".$active." - maximum: ".$queues, LOGGER_DEBUG); + $s = q("SELECT COUNT(*) AS `total` FROM `workerqueue` WHERE `executed` = '0000-00-00 00:00:00'"); + $entries = $s[0]["total"]; + if (Config::get("system", "worker_fastlane", false) AND ($queues > 0) AND ($entries > 0) AND ($active >= $queues)) { + $s = q("SELECT `priority` FROM `workerqueue` WHERE `executed` = '0000-00-00 00:00:00' ORDER BY `priority` LIMIT 1"); + $top_priority = $s[0]["priority"]; + + $s = q("SELECT `id` FROM `workerqueue` WHERE `priority` <= %d AND `executed` != '0000-00-00 00:00:00' LIMIT 1", + intval($top_priority)); + $high_running = dbm::is_result($s); + + if (!$high_running AND ($top_priority > PRIORITY_UNDEFINED) AND ($top_priority < PRIORITY_NEGLIGIBLE)) { + logger("There are jobs with priority ".$top_priority." waiting but none is executed. Open a fastlane.", LOGGER_DEBUG); + $queues = $active + 1; + } + } + + // Create a list of queue entries grouped by their priority + $running = array(PRIORITY_CRITICAL => 0, + PRIORITY_HIGH => 0, + PRIORITY_MEDIUM => 0, + PRIORITY_LOW => 0, + PRIORITY_NEGLIGIBLE => 0); + + $r = q("SELECT COUNT(*) AS `running`, `priority` FROM `process` INNER JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` GROUP BY `priority`"); + if (dbm::is_result($r)) + foreach ($r AS $process) + $running[$process["priority"]] = $process["running"]; + + $processlist = ""; + $r = q("SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` GROUP BY `priority`"); + if (dbm::is_result($r)) + foreach ($r as $entry) { + if ($processlist != "") + $processlist .= ", "; + $processlist .= $entry["priority"].":".$running[$entry["priority"]]."/".$entry["entries"]; + } + + logger("Load: ".$load."/".$maxsysload." - processes: ".$active."/".$entries." (".$processlist.") - maximum: ".$queues."/".$maxqueues, LOGGER_DEBUG); + + // Are there fewer workers running as possible? Then fork a new one. + if (!Config::get("system", "worker_dont_fork") AND ($queues > ($active + 1)) AND ($entries > 1)) { + logger("Active workers: ".$active."/".$queues." Fork a new worker.", LOGGER_DEBUG); + $args = array("php", "include/poller.php", "no_cron"); + $a = get_app(); + $a->proc_run($args); + } } return($active >= $queues); } +/** + * @brief Returns the number of active poller processes + * + * @return integer Number of active poller processes + */ function poller_active_workers() { - $workers = q("SELECT COUNT(*) AS `workers` FROM `workerqueue` WHERE `executed` != '0000-00-00 00:00:00'"); + $workers = q("SELECT COUNT(*) AS `processes` FROM `process` WHERE `command` = 'poller.php'"); - return($workers[0]["workers"]); + return($workers[0]["processes"]); +} + +/** + * @brief Check if we should pass some slow processes + * + * When the active processes of the highest priority are using more than 2/3 + * of all processes, we let pass slower processes. + * + * @param string $highest_priority Returns the currently highest priority + * @return bool We let pass a slower process than $highest_priority + */ +function poller_passing_slow(&$highest_priority) { + + $highest_priority = 0; + + $r = q("SELECT `priority` + FROM `process` + INNER JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid`"); + + // No active processes at all? Fine + if (!dbm::is_result($r)) + return(false); + + $priorities = array(); + foreach ($r AS $line) + $priorities[] = $line["priority"]; + + // Should not happen + if (count($priorities) == 0) + return(false); + + $highest_priority = min($priorities); + + // The highest process is already the slowest one? + // Then we quit + if ($highest_priority == PRIORITY_NEGLIGIBLE) + return(false); + + $high = 0; + foreach ($priorities AS $priority) + if ($priority == $highest_priority) + ++$high; + + logger("Highest priority: ".$highest_priority." Total processes: ".count($priorities)." Count high priority processes: ".$high, LOGGER_DEBUG); + $passing_slow = (($high/count($priorities)) > (2/3)); + + if ($passing_slow) + logger("Passing slower processes than priority ".$highest_priority, LOGGER_DEBUG); + + return($passing_slow); +} + +/** + * @brief Returns the next worker process + * + * @return string SQL statement + */ +function poller_worker_process() { + + q("START TRANSACTION;"); + + // Check if we should pass some low priority process + $highest_priority = 0; + + if (poller_passing_slow($highest_priority)) { + // Are there waiting processes with a higher priority than the currently highest? + $r = q("SELECT * FROM `workerqueue` + WHERE `executed` = '0000-00-00 00:00:00' AND `priority` < %d + ORDER BY `priority`, `created` LIMIT 1", dbesc($highest_priority)); + if (dbm::is_result($r)) + return $r; + + // Give slower processes some processing time + $r = q("SELECT * FROM `workerqueue` + WHERE `executed` = '0000-00-00 00:00:00' AND `priority` > %d + ORDER BY `priority`, `created` LIMIT 1", dbesc($highest_priority)); + } + + // If there is no result (or we shouldn't pass lower processes) we check without priority limit + if (($highest_priority == 0) OR !dbm::is_result($r)) + $r = q("SELECT * FROM `workerqueue` WHERE `executed` = '0000-00-00 00:00:00' ORDER BY `priority`, `created` LIMIT 1"); + + return $r; +} + +/** + * @brief Call the front end worker + */ +function call_worker() { + if (!Config::get("system", "frontend_worker") OR !Config::get("system", "worker")) { + return; + } + + $url = App::get_baseurl()."/worker"; + fetch_url($url, false, $redirects, 1); +} + +/** + * @brief Call the front end worker if there aren't any active + */ +function call_worker_if_idle() { + if (!Config::get("system", "frontend_worker") OR !Config::get("system", "worker")) { + return; + } + + // Do we have "proc_open"? Then we can fork the poller + if (function_exists("proc_open")) { + // When was the last time that we called the worker? + // Less than one minute? Then we quit + if ((time() - Config::get("system", "worker_started")) < 60) { + return; + } + + set_config("system", "worker_started", time()); + + // Do we have enough running workers? Then we quit here. + if (poller_too_much_workers()) { + // Cleaning dead processes + poller_kill_stale_workers(); + get_app()->remove_inactive_processes(); + + return; + } + + poller_run_cron(); + + logger('Call poller', LOGGER_DEBUG); + + $args = array("php", "include/poller.php", "no_cron"); + $a = get_app(); + $a->proc_run($args); + return; + } + + // We cannot execute background processes. + // We now run the processes from the frontend. + // This won't work with long running processes. + poller_run_cron(); + + clear_worker_processes(); + + $workers = q("SELECT COUNT(*) AS `processes` FROM `process` WHERE `command` = 'worker.php'"); + + if ($workers[0]["processes"] == 0) { + call_worker(); + } +} + +/** + * @brief Removes long running worker processes + */ +function clear_worker_processes() { + $timeout = Config::get("system", "frontend_worker_timeout", 10); + + /// @todo We should clean up the corresponding workerqueue entries as well + q("DELETE FROM `process` WHERE `created` < '%s' AND `command` = 'worker.php'", + dbesc(datetime_convert('UTC','UTC',"now - ".$timeout." minutes"))); +} + +/** + * @brief Runs the cron processes + */ +function poller_run_cron() { + logger('Add cron entries', LOGGER_DEBUG); + + // Check for spooled items + proc_run(PRIORITY_HIGH, "include/spool_post.php"); + + // Run the cron job that calls all other jobs + proc_run(PRIORITY_MEDIUM, "include/cron.php"); + + // Run the cronhooks job separately from cron for being able to use a different timing + proc_run(PRIORITY_MEDIUM, "include/cronhooks.php"); + + // Cleaning dead processes + poller_kill_stale_workers(); } if (array_search(__file__,get_included_files())===0){ - poller_run($_SERVER["argv"],$_SERVER["argc"]); - killme(); + poller_run($_SERVER["argv"],$_SERVER["argc"]); + + get_app()->end_process(); + + killme(); } ?> diff --git a/include/post_update.php b/include/post_update.php new file mode 100644 index 0000000000..f9649961d9 --- /dev/null +++ b/include/post_update.php @@ -0,0 +1,262 @@ += 1192) + return true; + + // Check if the first step is done (Setting "gcontact-id" in the item table) + $r = q("SELECT `author-link`, `author-name`, `author-avatar`, `uid`, `network` FROM `item` WHERE `gcontact-id` = 0 LIMIT 1000"); + if (!$r) { + // Are there unfinished entries in the thread table? + $r = q("SELECT COUNT(*) AS `total` FROM `thread` + INNER JOIN `item` ON `item`.`id` =`thread`.`iid` + WHERE `thread`.`gcontact-id` = 0 AND + (`thread`.`uid` IN (SELECT `uid` from `user`) OR `thread`.`uid` = 0)"); + + if ($r AND ($r[0]["total"] == 0)) { + set_config("system", "post_update_version", 1192); + return true; + } + + // Update the thread table from the item table + q("UPDATE `thread` INNER JOIN `item` ON `item`.`id`=`thread`.`iid` + SET `thread`.`gcontact-id` = `item`.`gcontact-id` + WHERE `thread`.`gcontact-id` = 0 AND + (`thread`.`uid` IN (SELECT `uid` from `user`) OR `thread`.`uid` = 0)"); + + return false; + } + + $item_arr = array(); + foreach ($r AS $item) { + $index = $item["author-link"]."-".$item["uid"]; + $item_arr[$index] = array("author-link" => $item["author-link"], + "uid" => $item["uid"], + "network" => $item["network"]); + } + + // Set the "gcontact-id" in the item table and add a new gcontact entry if needed + foreach($item_arr AS $item) { + $gcontact_id = get_gcontact_id(array("url" => $item['author-link'], "network" => $item['network'], + "photo" => $item['author-avatar'], "name" => $item['author-name'])); + q("UPDATE `item` SET `gcontact-id` = %d WHERE `uid` = %d AND `author-link` = '%s' AND `gcontact-id` = 0", + intval($gcontact_id), intval($item["uid"]), dbesc($item["author-link"])); + } + return false; +} + +/** + * @brief Updates the "global" field in the item table + * + * @return bool "true" when the job is done + */ +function post_update_1194() { + + // Was the script completed? + if (get_config("system", "post_update_version") >= 1194) + return true; + + logger("Start", LOGGER_DEBUG); + + $end_id = get_config("system", "post_update_1194_end"); + if (!$end_id) { + $r = q("SELECT `id` FROM `item` WHERE `uid` != 0 ORDER BY `id` DESC LIMIT 1"); + if ($r) { + set_config("system", "post_update_1194_end", $r[0]["id"]); + $end_id = get_config("system", "post_update_1194_end"); + } + } + + logger("End ID: ".$end_id, LOGGER_DEBUG); + + $start_id = get_config("system", "post_update_1194_start"); + + $query1 = "SELECT `item`.`id` FROM `item` "; + + $query2 = "INNER JOIN `item` AS `shadow` ON `item`.`uri` = `shadow`.`uri` AND `shadow`.`uid` = 0 "; + + $query3 = "WHERE `item`.`uid` != 0 AND `item`.`id` >= %d AND `item`.`id` <= %d + AND `item`.`visible` AND NOT `item`.`private` + AND NOT `item`.`deleted` AND NOT `item`.`moderated` + AND `item`.`network` IN ('%s', '%s', '%s', '') + AND `item`.`allow_cid` = '' AND `item`.`allow_gid` = '' + AND `item`.`deny_cid` = '' AND `item`.`deny_gid` = '' + AND NOT `item`.`global`"; + + $r = q($query1.$query2.$query3." ORDER BY `item`.`id` LIMIT 1", + intval($start_id), intval($end_id), + dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS)); + if (!$r) { + set_config("system", "post_update_version", 1194); + logger("Update is done", LOGGER_DEBUG); + return true; + } else { + set_config("system", "post_update_1194_start", $r[0]["id"]); + $start_id = get_config("system", "post_update_1194_start"); + } + + logger("Start ID: ".$start_id, LOGGER_DEBUG); + + $r = q($query1.$query2.$query3." ORDER BY `item`.`id` LIMIT 1000,1", + intval($start_id), intval($end_id), + dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS)); + if ($r) + $pos_id = $r[0]["id"]; + else + $pos_id = $end_id; + + logger("Progress: Start: ".$start_id." position: ".$pos_id." end: ".$end_id, LOGGER_DEBUG); + + $r = q("UPDATE `item` ".$query2." SET `item`.`global` = 1 ".$query3, + intval($start_id), intval($pos_id), + dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA), dbesc(NETWORK_OSTATUS)); + + logger("Done", LOGGER_DEBUG); +} + +/** + * @brief set the author-id and owner-id in all item entries + * + * This job has to be started multiple times until all entries are set. + * It isn't started in the update function since it would consume too much time and can be done in the background. + * + * @return bool "true" when the job is done + */ +function post_update_1198() { + + // Was the script completed? + if (get_config("system", "post_update_version") >= 1198) + return true; + + logger("Start", LOGGER_DEBUG); + + // Check if the first step is done (Setting "author-id" and "owner-id" in the item table) + $r = q("SELECT `author-link`, `owner-link`, `uid` FROM `item` WHERE `author-id` = 0 AND `owner-id` = 0 LIMIT 100"); + if (!$r) { + // Are there unfinished entries in the thread table? + $r = q("SELECT COUNT(*) AS `total` FROM `thread` + INNER JOIN `item` ON `item`.`id` =`thread`.`iid` + WHERE `thread`.`author-id` = 0 AND `thread`.`owner-id` = 0 AND + (`thread`.`uid` IN (SELECT `uid` from `user`) OR `thread`.`uid` = 0)"); + + if ($r AND ($r[0]["total"] == 0)) { + set_config("system", "post_update_version", 1198); + logger("Done", LOGGER_DEBUG); + return true; + } + + // Update the thread table from the item table + $r = q("UPDATE `thread` INNER JOIN `item` ON `item`.`id`=`thread`.`iid` + SET `thread`.`author-id` = `item`.`author-id`, + `thread`.`owner-id` = `item`.`owner-id` + WHERE `thread`.`author-id` = 0 AND `thread`.`owner-id` = 0 AND + (`thread`.`uid` IN (SELECT `uid` from `user`) OR `thread`.`uid` = 0)"); + + logger("Updated threads", LOGGER_DEBUG); + if (dbm::is_result($r)) { + set_config("system", "post_update_version", 1198); + logger("Done", LOGGER_DEBUG); + return true; + } + return false; + } + + logger("Query done", LOGGER_DEBUG); + + $item_arr = array(); + foreach ($r AS $item) { + $index = $item["author-link"]."-".$item["owner-link"]."-".$item["uid"]; + $item_arr[$index] = array("author-link" => $item["author-link"], + "owner-link" => $item["owner-link"], + "uid" => $item["uid"]); + } + + // Set the "gcontact-id" in the item table and add a new gcontact entry if needed + foreach($item_arr AS $item) { + $author_id = get_contact($item["author-link"], 0); + $owner_id = get_contact($item["owner-link"], 0); + + if ($author_id == 0) + $author_id = -1; + + if ($owner_id == 0) + $owner_id = -1; + + q("UPDATE `item` SET `author-id` = %d, `owner-id` = %d + WHERE `uid` = %d AND `author-link` = '%s' AND `owner-link` = '%s' + AND `author-id` = 0 AND `owner-id` = 0", + intval($author_id), intval($owner_id), intval($item["uid"]), + dbesc($item["author-link"]), dbesc($item["owner-link"])); + } + + logger("Updated items", LOGGER_DEBUG); + return false; +} + +/** + * @brief update the "last-item" field in the "self" contact + * + * This field avoids cost intensive calls in the admin panel and in "nodeinfo" + * + * @return bool "true" when the job is done + */ +function post_update_1206() { + // Was the script completed? + if (get_config("system", "post_update_version") >= 1206) + return true; + + logger("Start", LOGGER_DEBUG); + $r = q("SELECT `contact`.`id`, `contact`.`last-item`, + (SELECT MAX(`changed`) FROM `item` USE INDEX (`uid_wall_changed`) WHERE `wall` AND `uid` = `user`.`uid`) AS `lastitem_date` + FROM `user` + INNER JOIN `contact` ON `contact`.`uid` = `user`.`uid` AND `contact`.`self`"); + + if (!dbm::is_result($r)) { + return false; + } + foreach ($r AS $user) { + if (!empty($user["lastitem_date"]) AND ($user["lastitem_date"] > $user["last-item"])) { + q("UPDATE `contact` SET `last-item` = '%s' WHERE `id` = %d", + dbesc($user["lastitem_date"]), + intval($user["id"])); + } + } + + set_config("system", "post_update_version", 1206); + logger("Done", LOGGER_DEBUG); + return true; +} + +?> diff --git a/include/profile_update.php b/include/profile_update.php index 0fcf3617fb..7aa34d45d7 100644 --- a/include/profile_update.php +++ b/include/profile_update.php @@ -1,107 +1,6 @@ get_baseurl() . '/profile/' . $a->user['nickname']; -// if($url && strlen(get_config('system','directory'))) -// proc_run('php',"include/directory.php","$url"); - - $recips = q("SELECT `id`,`name`,`network`,`pubkey`,`notify` FROM `contact` WHERE `network` = '%s' - AND `uid` = %d AND `rel` != %d ", - dbesc(NETWORK_DIASPORA), - intval(local_user()), - intval(CONTACT_IS_SHARING) - ); - if(! count($recips)) - return; - - $r = q("SELECT `profile`.`uid` AS `profile_uid`, `profile`.* , `user`.* FROM `profile` - INNER JOIN `user` ON `profile`.`uid` = `user`.`uid` - WHERE `user`.`uid` = %d AND `profile`.`is-default` = 1 LIMIT 1", - intval(local_user()) - ); - - if(! count($r)) - return; - $profile = $r[0]; - - $handle = xmlify($a->user['nickname'] . '@' . substr($a->get_baseurl(), strpos($a->get_baseurl(),'://') + 3)); - $first = xmlify(((strpos($profile['name'],' ')) - ? trim(substr($profile['name'],0,strpos($profile['name'],' '))) : $profile['name'])); - $last = xmlify((($first === $profile['name']) ? '' : trim(substr($profile['name'],strlen($first))))); - $large = xmlify($a->get_baseurl() . '/photo/custom/300/' . $profile['uid'] . '.jpg'); - $medium = xmlify($a->get_baseurl() . '/photo/custom/100/' . $profile['uid'] . '.jpg'); - $small = xmlify($a->get_baseurl() . '/photo/custom/50/' . $profile['uid'] . '.jpg'); - $searchable = xmlify((($profile['publish'] && $profile['net-publish']) ? 'true' : 'false' )); -// $searchable = 'true'; - - if($searchable === 'true') { - $dob = '1000-00-00'; - - if(($profile['dob']) && ($profile['dob'] != '0000-00-00')) - $dob = ((intval($profile['dob'])) ? intval($profile['dob']) : '1000') . '-' . datetime_convert('UTC','UTC',$profile['dob'],'m-d'); - $gender = xmlify($profile['gender']); - $about = xmlify($profile['about']); - require_once('include/bbcode.php'); - $about = xmlify(strip_tags(bbcode($about))); - $location = ''; - if($profile['locality']) - $location .= $profile['locality']; - if($profile['region']) { - if($location) - $location .= ', '; - $location .= $profile['region']; - } - if($profile['country-name']) { - if($location) - $location .= ', '; - $location .= $profile['country-name']; - } - $location = xmlify($location); - $tags = ''; - if($profile['pub_keywords']) { - $kw = str_replace(',',' ',$profile['pub_keywords']); - $kw = str_replace(' ',' ',$kw); - $arr = explode(' ',$profile['pub_keywords']); - if(count($arr)) { - for($x = 0; $x < 5; $x ++) { - if(trim($arr[$x])) - $tags .= '#' . trim($arr[$x]) . ' '; - } - } - } - $tags = xmlify(trim($tags)); - } - - $tpl = get_markup_template('diaspora_profile.tpl'); - - $msg = replace_macros($tpl,array( - '$handle' => $handle, - '$first' => $first, - '$last' => $last, - '$large' => $large, - '$medium' => $medium, - '$small' => $small, - '$dob' => $dob, - '$gender' => $gender, - '$about' => $about, - '$location' => $location, - '$searchable' => $searchable, - '$tags' => $tags - )); - logger('profile_change: ' . $msg, LOGGER_ALL); - - foreach($recips as $recip) { - $msgtosend = 'xml=' . urlencode(urlencode(diaspora_msg_build($msg,$a->user,$recip,$a->user['prvkey'],$recip['pubkey'],false))); - add_to_queue($recip['id'],NETWORK_DIASPORA,$msgtosend,false); - } + Diaspora::send_profile(local_user()); } diff --git a/include/pubsubpublish.php b/include/pubsubpublish.php index d27beea3d5..428103a971 100644 --- a/include/pubsubpublish.php +++ b/include/pubsubpublish.php @@ -2,58 +2,57 @@ require_once("boot.php"); require_once("include/ostatus.php"); -function handle_pubsubhubbub() { +use \Friendica\Core\Config; +use \Friendica\Core\PConfig; + +function handle_pubsubhubbub($id) { global $a, $db; - logger('start'); + $r = q("SELECT * FROM `push_subscriber` WHERE `id` = %d", intval($id)); + if (!$r) + return; + else + $rr = $r[0]; - // We'll push to each subscriber that has push > 0, - // i.e. there has been an update (set in notifier.php). + logger("Generate feed of user ".$rr['nickname']." to ".$rr['callback_url']." - last updated ".$rr['last_update'], LOGGER_DEBUG); - $r = q("SELECT * FROM `push_subscriber` WHERE `push` > 0"); + $params = ostatus::feed($a, $rr['nickname'], $rr['last_update']); + $hmac_sig = hash_hmac("sha1", $params, $rr['secret']); - foreach($r as $rr) { - //$params = get_feed_for($a, '', $rr['nickname'], $rr['last_update'], 0, true); - $params = ostatus_feed($a, $rr['nickname'], $rr['last_update']); - $hmac_sig = hash_hmac("sha1", $params, $rr['secret']); + $headers = array("Content-type: application/atom+xml", + sprintf("Link: <%s>;rel=hub,<%s>;rel=self", + App::get_baseurl().'/pubsubhubbub', + $rr['topic']), + "X-Hub-Signature: sha1=".$hmac_sig); - $headers = array("Content-type: application/atom+xml", - sprintf("Link: <%s>;rel=hub,<%s>;rel=self", - $a->get_baseurl().'/pubsubhubbub', - $rr['topic']), - "X-Hub-Signature: sha1=".$hmac_sig); + logger('POST '.print_r($headers, true)."\n".$params, LOGGER_DEBUG); - logger('POST '.print_r($headers, true)."\n".$params, LOGGER_DEBUG); + post_url($rr['callback_url'], $params, $headers); + $ret = $a->get_curl_code(); - post_url($rr['callback_url'], $params, $headers); - $ret = $a->get_curl_code(); + if ($ret >= 200 && $ret <= 299) { + logger('successfully pushed to '.$rr['callback_url']); - if ($ret >= 200 && $ret <= 299) { - logger('successfully pushed to '.$rr['callback_url']); + // set last_update to "now", and reset push=0 + $date_now = datetime_convert('UTC','UTC','now','Y-m-d H:i:s'); + q("UPDATE `push_subscriber` SET `push` = 0, last_update = '%s' WHERE id = %d", + dbesc($date_now), + intval($rr['id'])); - // set last_update to "now", and reset push=0 - $date_now = datetime_convert('UTC','UTC','now','Y-m-d H:i:s'); - q("UPDATE `push_subscriber` SET `push` = 0, last_update = '%s' WHERE id = %d", - dbesc($date_now), - intval($rr['id'])); + } else { + logger('error when pushing to '.$rr['callback_url'].' HTTP: '.$ret); - } else { - logger('error when pushing to '.$rr['callback_url'].' HTTP: '.$ret); + // we use the push variable also as a counter, if we failed we + // increment this until some upper limit where we give up + $new_push = intval($rr['push']) + 1; - // we use the push variable also as a counter, if we failed we - // increment this until some upper limit where we give up - $new_push = intval($rr['push']) + 1; + if ($new_push > 30) // OK, let's give up + $new_push = 0; - if ($new_push > 30) // OK, let's give up - $new_push = 0; - - q("UPDATE `push_subscriber` SET `push` = %d WHERE id = %d", - $new_push, - intval($rr['id'])); - } + q("UPDATE `push_subscriber` SET `push` = %d WHERE id = %d", + $new_push, + intval($rr['id'])); } - - logger('done'); } @@ -72,22 +71,12 @@ function pubsubpublish_run(&$argv, &$argc){ }; require_once('include/items.php'); - require_once('include/pidfile.php'); - load_config('config'); - load_config('system'); + Config::load(); - $lockpath = get_lockpath(); - if ($lockpath != '') { - $pidfile = new pidfile($lockpath, 'pubsubpublish'); - if($pidfile->is_already_running()) { - logger("Already running"); - if ($pidfile->running_time() > 9*60) { - $pidfile->kill(); - logger("killed stale process"); - // Calling a new instance - proc_run('php',"include/pubsubpublish.php"); - } + // Don't check this stuff if the function is called by the poller + if (App::callstack() != "poller_run") { + if (App::is_already_running("pubsubpublish", "include/pubsubpublish.php", 540)) { return; } } @@ -96,12 +85,32 @@ function pubsubpublish_run(&$argv, &$argc){ load_hooks(); - if($argc > 1) + if ($argc > 1) { $pubsubpublish_id = intval($argv[1]); - else - $pubsubpublish_id = 0; + } + else { + // We'll push to each subscriber that has push > 0, + // i.e. there has been an update (set in notifier.php). + $r = q("SELECT `id`, `callback_url` FROM `push_subscriber` WHERE `push` > 0"); - handle_pubsubhubbub(); + // Use the delivery interval that is also used for the notifier + $interval = Config::get("system", "delivery_interval", 2); + + // If we are using the worker we don't need a delivery interval + if (get_config("system", "worker")) { + $interval = false; + } + + foreach ($r as $rr) { + logger("Publish feed to ".$rr["callback_url"], LOGGER_DEBUG); + proc_run(PRIORITY_HIGH, 'include/pubsubpublish.php', $rr["id"]); + + if($interval) + @time_sleep_until(microtime(true) + (float) $interval); + } + } + + handle_pubsubhubbub($pubsubpublish_id); return; diff --git a/include/queue.php b/include/queue.php index cb5fe28ad9..bcd32985db 100644 --- a/include/queue.php +++ b/include/queue.php @@ -1,6 +1,10 @@ is_already_running()) { - logger("queue: Already running"); - if ($pidfile->running_time() > 9*60) { - $pidfile->kill(); - logger("queue: killed stale process"); - // Calling a new instance - proc_run('php',"include/queue.php"); - } + // Don't check this stuff if the function is called by the poller + if (App::callstack() != "poller_run") + if (App::is_already_running('queue', 'include/queue.php', 540)) return; - } - } $a->set_baseurl(get_config('system','url')); @@ -52,56 +43,61 @@ function queue_run(&$argv, &$argc){ $queue_id = 0; $deadguys = array(); + $deadservers = array(); + $serverlist = array(); - logger('queue: start'); + if (!$queue_id) { - // Handling the pubsubhubbub requests - proc_run('php','include/pubsubpublish.php'); + logger('queue: start'); - $interval = ((get_config('system','delivery_interval') === false) ? 2 : intval(get_config('system','delivery_interval'))); + // Handling the pubsubhubbub requests + proc_run(PRIORITY_HIGH,'include/pubsubpublish.php'); - // If we are using the worker we don't need a delivery interval - if (get_config("system", "worker")) - $interval = false; + $interval = ((get_config('system','delivery_interval') === false) ? 2 : intval(get_config('system','delivery_interval'))); - $r = q("select * from deliverq where 1"); - if($r) { - foreach($r as $rr) { - logger('queue: deliverq'); - proc_run('php','include/delivery.php',$rr['cmd'],$rr['item'],$rr['contact']); - if($interval) - @time_sleep_until(microtime(true) + (float) $interval); + // If we are using the worker we don't need a delivery interval + if (get_config("system", "worker")) + $interval = false; + + $r = q("select * from deliverq where 1"); + if ($r) { + foreach ($r as $rr) { + logger('queue: deliverq'); + proc_run(PRIORITY_HIGH,'include/delivery.php',$rr['cmd'],$rr['item'],$rr['contact']); + if($interval) { + time_sleep_until(microtime(true) + (float) $interval); + } + } } - } - $r = q("SELECT `queue`.*, `contact`.`name`, `contact`.`uid` FROM `queue` - INNER JOIN `contact` ON `queue`.`cid` = `contact`.`id` - WHERE `queue`.`created` < UTC_TIMESTAMP() - INTERVAL 3 DAY"); - if($r) { - foreach($r as $rr) { - logger('Removing expired queue item for ' . $rr['name'] . ', uid=' . $rr['uid']); - logger('Expired queue data :' . $rr['content'], LOGGER_DATA); + $r = q("SELECT `queue`.*, `contact`.`name`, `contact`.`uid` FROM `queue` + INNER JOIN `contact` ON `queue`.`cid` = `contact`.`id` + WHERE `queue`.`created` < UTC_TIMESTAMP() - INTERVAL 3 DAY"); + if ($r) { + foreach ($r as $rr) { + logger('Removing expired queue item for ' . $rr['name'] . ', uid=' . $rr['uid']); + logger('Expired queue data :' . $rr['content'], LOGGER_DATA); + } + q("DELETE FROM `queue` WHERE `created` < UTC_TIMESTAMP() - INTERVAL 3 DAY"); } - q("DELETE FROM `queue` WHERE `created` < UTC_TIMESTAMP() - INTERVAL 3 DAY"); - } - - if($queue_id) { - $r = q("SELECT `id` FROM `queue` WHERE `id` = %d LIMIT 1", - intval($queue_id) - ); - } - else { // For the first 12 hours we'll try to deliver every 15 minutes // After that, we'll only attempt delivery once per hour. - $r = q("SELECT `id` FROM `queue` WHERE (( `created` > UTC_TIMESTAMP() - INTERVAL 12 HOUR && `last` < UTC_TIMESTAMP() - INTERVAL 15 MINUTE ) OR ( `last` < UTC_TIMESTAMP() - INTERVAL 1 HOUR ))"); + $r = q("SELECT `id` FROM `queue` WHERE ((`created` > UTC_TIMESTAMP() - INTERVAL 12 HOUR && `last` < UTC_TIMESTAMP() - INTERVAL 15 MINUTE) OR (`last` < UTC_TIMESTAMP() - INTERVAL 1 HOUR)) ORDER BY `cid`, `created`"); + } else { + logger('queue: start for id '.$queue_id); + + $r = q("SELECT `id` FROM `queue` WHERE `id` = %d LIMIT 1", + intval($queue_id) + ); } - if(! $r){ + + if (!$r){ return; } - if(! $queue_id) + if (!$queue_id) call_hooks('queue_predeliver', $a, $r); @@ -115,16 +111,17 @@ function queue_run(&$argv, &$argc){ // queue_predeliver hooks may have changed the queue db details, // so check again if this entry still needs processing - if($queue_id) { - $qi = q("select * from queue where `id` = %d limit 1", - intval($queue_id) - ); - } - else { + if($queue_id) + $qi = q("SELECT * FROM `queue` WHERE `id` = %d LIMIT 1", + intval($queue_id)); + elseif (get_config("system", "worker")) { + logger('Call queue for id '.$q_item['id']); + proc_run(PRIORITY_LOW, "include/queue.php", $q_item['id']); + continue; + } else $qi = q("SELECT * FROM `queue` WHERE `id` = %d AND `last` < UTC_TIMESTAMP() - INTERVAL 15 MINUTE ", - intval($q_item['id']) - ); - } + intval($q_item['id'])); + if(! count($qi)) continue; @@ -132,7 +129,7 @@ function queue_run(&$argv, &$argc){ $c = q("SELECT * FROM `contact` WHERE `id` = %d LIMIT 1", intval($qi[0]['cid']) ); - if(! count($c)) { + if (! dbm::is_result($c)) { remove_queue_item($q_item['id']); continue; } @@ -142,8 +139,18 @@ function queue_run(&$argv, &$argc){ continue; } - if (!poco_reachable($c[0]['url'])) { - logger('queue: skipping probably dead url: ' . $c[0]['url']); + $server = poco_detect_server($c[0]['url']); + + if (($server != "") AND !in_array($server, $serverlist)) { + logger("Check server ".$server." (".$c[0]["network"].")"); + if (!poco_check_server($server, $c[0]["network"], true)) + $deadservers[] = $server; + + $serverlist[] = $server; + } + + if (($server != "") AND in_array($server, $deadservers)) { + logger('queue: skipping known dead server: '.$server); update_queue_time($q_item['id']); continue; } @@ -152,7 +159,7 @@ function queue_run(&$argv, &$argc){ FROM `user` WHERE `uid` = %d LIMIT 1", intval($c[0]['uid']) ); - if(! count($u)) { + if (! dbm::is_result($u)) { remove_queue_item($q_item['id']); continue; } @@ -166,37 +173,39 @@ function queue_run(&$argv, &$argc){ switch($contact['network']) { case NETWORK_DFRN: - logger('queue: dfrndelivery: item ' . $q_item['id'] . ' for ' . $contact['name']); - $deliver_status = dfrn_deliver($owner,$contact,$data); + logger('queue: dfrndelivery: item '.$q_item['id'].' for '.$contact['name'].' <'.$contact['url'].'>'); + $deliver_status = dfrn::deliver($owner,$contact,$data); if($deliver_status == (-1)) { update_queue_time($q_item['id']); $deadguys[] = $contact['notify']; - } - else { + } else remove_queue_item($q_item['id']); - } + break; case NETWORK_OSTATUS: if($contact['notify']) { - logger('queue: slapdelivery: item ' . $q_item['id'] . ' for ' . $contact['name']); + logger('queue: slapdelivery: item '.$q_item['id'].' for '.$contact['name'].' <'.$contact['url'].'>'); $deliver_status = slapper($owner,$contact['notify'],$data); - if($deliver_status == (-1)) + if($deliver_status == (-1)) { update_queue_time($q_item['id']); - else + $deadguys[] = $contact['notify']; + } else remove_queue_item($q_item['id']); } break; case NETWORK_DIASPORA: if($contact['notify']) { - logger('queue: diaspora_delivery: item ' . $q_item['id'] . ' for ' . $contact['name']); - $deliver_status = diaspora_transmit($owner,$contact,$data,$public,true); + logger('queue: diaspora_delivery: item '.$q_item['id'].' for '.$contact['name'].' <'.$contact['url'].'>'); + $deliver_status = Diaspora::transmit($owner,$contact,$data,$public,true); - if($deliver_status == (-1)) + if($deliver_status == (-1)) { update_queue_time($q_item['id']); - else + $deadguys[] = $contact['notify']; + } else remove_queue_item($q_item['id']); + } break; @@ -212,6 +221,7 @@ function queue_run(&$argv, &$argc){ break; } + logger('Deliver status '.$deliver_status.' for item '.$q_item['id'].' to '.$contact['name'].' <'.$contact['url'].'>'); } return; diff --git a/include/queue_fn.php b/include/queue_fn.php index 5214131b2b..9dcefdd244 100644 --- a/include/queue_fn.php +++ b/include/queue_fn.php @@ -15,22 +15,34 @@ function remove_queue_item($id) { ); } +/** + * @brief Checks if the communication with a given contact had problems recently + * + * @param int $cid Contact id + * + * @return bool The communication with this contact has currently problems + */ function was_recently_delayed($cid) { + $was_delayed = false; - $r = q("SELECT `id` FROM `queue` WHERE `cid` = %d - and last > UTC_TIMESTAMP() - interval 15 minute limit 1", + // Are there queue entries that were recently added? + $r = q("SELECT `id` FROM `queue` WHERE `cid` = %d + AND `last` > UTC_TIMESTAMP() - INTERVAL 15 MINUTE LIMIT 1", intval($cid) ); - if(count($r)) - return true; - $r = q("select `term-date` from contact where id = %d and `term-date` != '' and `term-date` != '0000-00-00 00:00:00' limit 1", - intval($cid) - ); - if(count($r)) - return true; + $was_delayed = dbm::is_result($r); - return false; + // We set "term-date" to a current date if the communication has problems. + // If the communication works again we reset this value. + if ($was_delayed) { + $r = q("SELECT `term-date` FROM `contact` WHERE `id` = %d AND `term-date` <= '1000-01-01' LIMIT 1", + intval($cid) + ); + $was_delayed = !dbm::is_result($r); + } + + return $was_delayed; } @@ -48,7 +60,7 @@ function add_to_queue($cid,$network,$msg,$batch = false) { WHERE `queue`.`cid` = %d AND `contact`.`self` = 0 ", intval($cid) ); - if($r && count($r)) { + if (dbm::is_result($r)) { if($batch && ($r[0]['total'] > $batch_queue)) { logger('add_to_queue: too many queued items for batch server ' . $cid . ' - discarding message'); return; diff --git a/include/redir.php b/include/redir.php index ab4f3220cd..76e30a6eac 100644 --- a/include/redir.php +++ b/include/redir.php @@ -1,6 +1,6 @@ get_baseurl(); + $baseurl = App::get_baseurl(); $domain_st = strpos($baseurl, "://"); if($domain_st === false) return; $baseurl = substr($baseurl, $domain_st + 3); $nurl = normalise_link($baseurl); - - $r = q("SELECT id FROM contact WHERE uid = ( SELECT uid FROM user WHERE nickname = '%s' LIMIT 1 ) - AND nick = '%s' AND self = 0 AND ( url LIKE '%%%s%%' or nurl LIKE '%%%s%%' ) AND blocked = 0 AND pending = 0 LIMIT 1", - dbesc($contact_nick), - dbesc($a->user['nickname']), - dbesc($baseurl), - dbesc($nurl) + /// @todo Why is there a query for "url" *and* "nurl"? Especially this normalising is strange. + $r = q("SELECT `id` FROM `contact` WHERE `uid` = (SELECT `uid` FROM `user` WHERE `nickname` = '%s' LIMIT 1) + AND `nick` = '%s' AND NOT `self` AND (`url` LIKE '%%%s%%' OR `nurl` LIKE '%%%s%%') AND NOT `blocked` AND NOT `pending` LIMIT 1", + dbesc($contact_nick), + dbesc($a->user['nickname']), + dbesc($baseurl), + dbesc($nurl) ); - if((!$r) || (! count($r)) || $r[0]['id'] == remote_user()) + if ((! dbm::is_result($r)) || $r[0]['id'] == remote_user()) { return; - + } $r = q("SELECT * FROM contact WHERE nick = '%s' AND network = '%s' AND uid = %d AND url LIKE '%%%s%%' LIMIT 1", @@ -48,8 +48,9 @@ function auto_redir(&$a, $contact_nick) { dbesc($baseurl) ); - if(! ($r && count($r))) + if (! dbm::is_result($r)) { return; + } $cid = $r[0]['id']; @@ -69,7 +70,7 @@ function auto_redir(&$a, $contact_nick) { if(strlen($dfrn_id) < 3) return; - + $sec = random_string(); q("INSERT INTO `profile_check` ( `uid`, `cid`, `dfrn_id`, `sec`, `expire`) @@ -83,9 +84,9 @@ function auto_redir(&$a, $contact_nick) { $url = curPageURL(); - logger('auto_redir: ' . $r[0]['name'] . ' ' . $sec, LOGGER_DEBUG); + logger('auto_redir: ' . $r[0]['name'] . ' ' . $sec, LOGGER_DEBUG); $dest = (($url) ? '&destination_url=' . $url : ''); - goaway ($r[0]['poll'] . '?dfrn_id=' . $dfrn_id + goaway ($r[0]['poll'] . '?dfrn_id=' . $dfrn_id . '&dfrn_version=' . DFRN_PROTOCOL_VERSION . '&type=profile&sec=' . $sec . $dest ); } diff --git a/include/remoteupdate.php b/include/remoteupdate.php deleted file mode 100644 index 9effc9b6e9..0000000000 --- a/include/remoteupdate.php +++ /dev/null @@ -1,261 +0,0 @@ -tags as $i=>$v){ - $i = (float)$i; - if ($i>$tag) $tag=$i; - } - - if ($tag==0.0) return false; - $f = fetch_url("https://raw.github.com/".F9KREPO."/".$tag."/boot.php","r"); - preg_match("|'FRIENDICA_VERSION', *'([^']*)'|", $f, $m); - $version = $m[1]; - - $lv = explode(".", FRIENDICA_VERSION); - $rv = explode(".",$version); - foreach($lv as $i=>$v){ - if ((int)$lv[$i] < (int)$rv[$i]) { - return array($tag, $version, "https://github.com/friendica/friendica/zipball/".$tag); - break; - } - } - return false; -} -function canWeWrite(){ - $bd = dirname(dirname(__file__)); - return is_writable( $bd."/boot.php" ); -} - -function out($txt){ echo "§".$txt."§"; ob_end_flush(); flush();} - -function up_count($path){ - - $file_count = 0; - - $dir_handle = opendir($path); - - if (!$dir_handle) return -1; - - while ($file = readdir($dir_handle)) { - - if ($file == '.' || $file == '..') continue; - $file_count++; - - if (is_dir($path . $file)){ - $file_count += up_count($path . $file . DIRECTORY_SEPARATOR); - } - - } - - closedir($dir_handle); - - return $file_count; -} - - - -function up_unzip($file, $folder="/tmp"){ - $folder.="/"; - $zip = zip_open($file); - if ($zip) { - while ($zip_entry = zip_read($zip)) { - $zip_entry_name = zip_entry_name($zip_entry); - if (substr($zip_entry_name,strlen($zip_entry_name)-1,1)=="/"){ - mkdir($folder.$zip_entry_name,0777, true); - } else { - $fp = fopen($folder.$zip_entry_name, "w"); - if (zip_entry_open($zip, $zip_entry, "r")) { - $buf = zip_entry_read($zip_entry, zip_entry_filesize($zip_entry)); - fwrite($fp,"$buf"); - zip_entry_close($zip_entry); - fclose($fp); - } - } - } - zip_close($zip); - } -} - -/** - * Walk recoursively in a folder and call a callback function on every - * dir entry. - * args: - * $dir string base dir to walk - * $callback function callback function - * $sort int 0: ascending, 1: descending - * $cb_argv any extra value passed to callback - * - * callback signature: - * function name($fn, $dir [, $argv]) - * $fn string full dir entry name - * $dir string start dir path - * $argv any user value to callback - * - */ -function up_walktree($dir, $callback=Null, $sort=0, $cb_argv=Null , $startdir=Null){ - if (is_null($callback)) return; - if (is_null($startdir)) $startdir = $dir; - $res = scandir($dir, $sort); - foreach($res as $i=>$v){ - if ($v!="." && $v!=".."){ - $fn = $dir."/".$v; - if ($sort==0) $callback($fn, $startdir, $cb_argv); - if (is_dir($fn)) up_walktree($fn, $callback, $sort, $cb_argv, $startdir); - if ($sort==1) $callback($fn, $startdir, $cb_argv); - } - } - -} - -function up_copy($fn, $dir){ - global $up_countfiles, $up_totalfiles, $up_lastp; - $up_countfiles++; $prc=(int)(((float)$up_countfiles/(float)$up_totalfiles)*100); - - if (strpos($fn, ".gitignore")>-1 || strpos($fn, ".htaccess")>-1) return; - $ddest = dirname(dirname(__file__)); - $fd = str_replace($dir, $ddest, $fn); - - if (is_dir($fn) && !is_dir($fd)) { - $re=mkdir($fd,0777,true); - } - if (!is_dir($fn)){ - $re=copy($fn, $fd); - } - - if ($re===false) { - out("ERROR. Abort."); - killme(); - } - out("copy@Copy@$prc%"); -} - -function up_ftp($fn, $dir, $argv){ - global $up_countfiles, $up_totalfiles, $up_lastp; - $up_countfiles++; $prc=(int)(((float)$up_countfiles/(float)$up_totalfiles)*100); - - if (strpos($fn, ".gitignore")>-1 || strpos($fn, ".htaccess")>-1) return; - - list($ddest, $conn_id) = $argv; - $l = strlen($ddest)-1; - if (substr($ddest,$l,1)=="/") $ddest = substr($ddest,0,$l); - $fd = str_replace($dir, $ddest, $fn); - - if (is_dir($fn)){ - if (ftp_nlist($conn_id, $fd)===false) { - $ret = ftp_mkdir($conn_id, $fd); - } else { - $ret=true; - } - } else { - $ret = ftp_put($conn_id, $fd, $fn, FTP_BINARY); - } - if (!$ret) { - out("ERROR. Abort."); - killme(); - } - out("copy@Copy@$prc%"); -} - -function up_rm($fn, $dir){ - if (is_dir($fn)){ - rmdir($fn); - } else { - unlink($fn); - } -} - -function up_dlfile($url, $file) { - $in = fopen ($url, "r"); - $out = fopen ($file, "w"); - - $fs = filesize($url); - - - if (!$in || !$out) return false; - - $s=0; $count=0; - while (!feof ($in)) { - $line = fgets ($in, 1024); - fwrite( $out, $line); - - $count++; $s += strlen($line); - if ($count==50){ - $count=0; - $sp=$s/1024.0; $ex="Kb"; - if ($sp>1024) { $sp=$sp/1024; $ex="Mb"; } - if ($sp>1024) { $sp=$sp/1024; $ex="Gb"; } - $sp = ((int)($sp*100))/100; - out("dwl@Download@".$sp.$ex); - } - } - fclose($in); - return true; -} - -function doUpdate($remotefile, $ftpdata=false){ - global $up_totalfiles; - - - $localtmpfile = tempnam("/tmp", "fk"); - out("dwl@Download@starting..."); - $rt= up_dlfile($remotefile, $localtmpfile); - if ($rt==false || filesize($localtmpfile)==0){ - out("dwl@Download@ERROR."); - unlink($localtmpfile); - return; - } - out("dwl@Download@Ok."); - - out("unzip@Unzip@"); - $tmpdirname = $localfile."ex"; - mkdir($tmpdirname); - up_unzip($localtmpfile, $tmpdirname); - $basedir = glob($tmpdirname."/*"); $basedir=$basedir[0]; - out ("unzip@Unzip@Ok."); - - $up_totalfiles = up_count($basedir."/"); - - if (canWeWrite()){ - out("copy@Copy@"); - up_walktree($basedir, 'up_copy'); - } - if ($ftpdata!==false && is_array($ftpdata) && $ftpdata['ftphost']!="" ){ - out("ftpcon@Connect to FTP@"); - $conn_id = ftp_connect($ftpdata['ftphost']); - $login_result = ftp_login($conn_id, $ftpdata['ftpuser'], $ftpdata['ftppwd']); - - if ((!$conn_id) || (!$login_result)) { - out("ftpcon@Connect to FTP@FAILED"); - up_clean($tmpdirname, $localtmpfile); - return; - } else { - out("ftpcon@Connect to FTP@Ok."); - } - out("copy@Copy@"); - up_walktree($basedir, 'up_ftp', 0, array( $ftpdata['ftppath'], $conn_id)); - - ftp_close($conn_id); - } - - up_clean($tmpdirname, $localtmpfile); - -} - -function up_clean($tmpdirname, $localtmpfile){ - out("clean@Clean up@"); - unlink($localtmpfile); - up_walktree($tmpdirname, 'up_rm', 1); - rmdir($tmpdirname); - out("clean@Clean up@Ok."); -} diff --git a/include/remove_contact.php b/include/remove_contact.php new file mode 100644 index 0000000000..aa20621116 --- /dev/null +++ b/include/remove_contact.php @@ -0,0 +1,54 @@ + diff --git a/include/salmon.php b/include/salmon.php index a254fe7e97..2b58334704 100644 --- a/include/salmon.php +++ b/include/salmon.php @@ -1,15 +1,14 @@ 0) { + for ($x = 0; $x < count($ret); $x ++) { + if (substr($ret[$x],0,5) === 'data:') { + if (strstr($ret[$x],',')) { $ret[$x] = substr($ret[$x],strpos($ret[$x],',')+1); - else + } else { $ret[$x] = substr($ret[$x],5); - } - else + } + } elseif (normalise_link($ret[$x]) == 'http://') { $ret[$x] = fetch_url($ret[$x]); + } } } logger('Key located: ' . print_r($ret,true)); - if(count($ret) == 1) { + if (count($ret) == 1) { // We only found one one key so we don't care if the hash matches. // If it's the wrong key we'll find out soon enough because @@ -52,10 +52,11 @@ function get_salmon_key($uri,$keyhash) { return $ret[0]; } else { - foreach($ret as $a) { + foreach ($ret as $a) { $hash = base64url_encode(hash('sha256',$a)); - if($hash == $keyhash) + if ($hash == $keyhash) { return $a; + } } } @@ -78,23 +79,6 @@ function slapper($owner,$url,$slap) { return; } - // add all namespaces to item - -$namespaces = <<< EOT - > -EOT; - - $slap = str_replace('',$namespaces,$slap); - logger('slapper called for '.$url.'. Data: ' . $slap); // create a magic envelope diff --git a/include/security.php b/include/security.php index 2d6db95f4f..c379518562 100644 --- a/include/security.php +++ b/include/security.php @@ -9,8 +9,8 @@ function authenticate_success($user_record, $login_initial = false, $interactive $_SESSION['mobile-theme'] = get_pconfig($user_record['uid'], 'system', 'mobile_theme'); $_SESSION['authenticated'] = 1; $_SESSION['page_flags'] = $user_record['page-flags']; - $_SESSION['my_url'] = $a->get_baseurl() . '/profile/' . $user_record['nickname']; - $_SESSION['my_address'] = $user_record['nickname'] . '@' . substr($a->get_baseurl(),strpos($a->get_baseurl(),'://')+3); + $_SESSION['my_url'] = App::get_baseurl() . '/profile/' . $user_record['nickname']; + $_SESSION['my_address'] = $user_record['nickname'] . '@' . substr(App::get_baseurl(),strpos(App::get_baseurl(),'://')+3); $_SESSION['addr'] = $_SERVER['REMOTE_ADDR']; $a->user = $user_record; @@ -42,7 +42,7 @@ function authenticate_success($user_record, $login_initial = false, $interactive $r = q("select * from user where uid = %d limit 1", intval($_SESSION['submanage']) ); - if(count($r)) + if (dbm::is_result($r)) $master_record = $r[0]; } @@ -50,7 +50,7 @@ function authenticate_success($user_record, $login_initial = false, $interactive dbesc($master_record['password']), dbesc($master_record['email']) ); - if($r && count($r)) + if (dbm::is_result($r)) $a->identities = $r; else $a->identities = array(); @@ -60,7 +60,7 @@ function authenticate_success($user_record, $login_initial = false, $interactive and `manage`.`uid` = %d", intval($master_record['uid']) ); - if($r && count($r)) + if (dbm::is_result($r)) $a->identities = array_merge($a->identities,$r); if($login_initial) @@ -70,7 +70,7 @@ function authenticate_success($user_record, $login_initial = false, $interactive $r = q("SELECT * FROM `contact` WHERE `uid` = %d AND `self` = 1 LIMIT 1", intval($_SESSION['uid'])); - if(count($r)) { + if (dbm::is_result($r)) { $a->contact = $r[0]; $a->cid = $r[0]['id']; $_SESSION['cid'] = $a->cid; @@ -79,11 +79,9 @@ function authenticate_success($user_record, $login_initial = false, $interactive header('X-Account-Management-Status: active; name="' . $a->user['username'] . '"; id="' . $a->user['nickname'] .'"'); if($login_initial || $login_refresh) { - $l = get_browser_language(); - q("UPDATE `user` SET `login_date` = '%s', `language` = '%s' WHERE `uid` = %d", + q("UPDATE `user` SET `login_date` = '%s' WHERE `uid` = %d", dbesc(datetime_convert()), - dbesc($l), intval($_SESSION['uid']) ); @@ -96,31 +94,33 @@ function authenticate_success($user_record, $login_initial = false, $interactive } - if($login_initial) { + if ($login_initial) { call_hooks('logged_in', $a->user); - if(($a->module !== 'home') && isset($_SESSION['return_url'])) - goaway($a->get_baseurl() . '/' . $_SESSION['return_url']); + if (($a->module !== 'home') && isset($_SESSION['return_url'])) { + goaway(App::get_baseurl() . '/' . $_SESSION['return_url']); + } } } -function can_write_wall(&$a,$owner) { +function can_write_wall(App $a, $owner) { static $verified = 0; - if((! (local_user())) && (! (remote_user()))) + if ((! (local_user())) && (! (remote_user()))) { return false; + } $uid = local_user(); - if(($uid) && ($uid == $owner)) { + if (($uid) && ($uid == $owner)) { return true; } - if(remote_user()) { + if (remote_user()) { // use remembered decision and avoid a DB lookup for each and every display item // DO NOT use this function if there are going to be multiple owners @@ -128,28 +128,28 @@ function can_write_wall(&$a,$owner) { // We have a contact-id for an authenticated remote user, this block determines if the contact // belongs to this page owner, and has the necessary permissions to post content - if($verified === 2) + if ($verified === 2) { return true; - elseif($verified === 1) + } elseif ($verified === 1) { return false; - else { + } else { $cid = 0; - if(is_array($_SESSION['remote'])) { - foreach($_SESSION['remote'] as $visitor) { - if($visitor['uid'] == $owner) { + if (is_array($_SESSION['remote'])) { + foreach ($_SESSION['remote'] as $visitor) { + if ($visitor['uid'] == $owner) { $cid = $visitor['cid']; break; } } } - if(! $cid) + if (! $cid) { return false; + } - - $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `user`.`uid` = `contact`.`uid` - WHERE `contact`.`uid` = %d AND `contact`.`id` = %d AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0 + $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `user`.`uid` = `contact`.`uid` + WHERE `contact`.`uid` = %d AND `contact`.`id` = %d AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0 AND `user`.`blockwall` = 0 AND `readonly` = 0 AND ( `contact`.`rel` IN ( %d , %d ) OR `user`.`page-flags` = %d ) LIMIT 1", intval($owner), intval($cid), @@ -158,7 +158,7 @@ function can_write_wall(&$a,$owner) { intval(PAGE_COMMUNITY) ); - if(count($r)) { + if (dbm::is_result($r)) { $verified = 2; return true; } @@ -183,10 +183,10 @@ function permissions_sql($owner_id,$remote_verified = false,$groups = null) { * default permissions - anonymous user */ - $sql = " AND allow_cid = '' - AND allow_gid = '' - AND deny_cid = '' - AND deny_gid = '' + $sql = " AND allow_cid = '' + AND allow_gid = '' + AND deny_cid = '' + AND deny_gid = '' "; /** @@ -194,11 +194,11 @@ function permissions_sql($owner_id,$remote_verified = false,$groups = null) { */ if(($local_user) && ($local_user == $owner_id)) { - $sql = ''; + $sql = ''; } /** - * Authenticated visitor. Unless pre-verified, + * Authenticated visitor. Unless pre-verified, * check that the contact belongs to this $owner_id * and load the groups the visitor belongs to. * If pre-verified, the caller is expected to have already @@ -212,7 +212,7 @@ function permissions_sql($owner_id,$remote_verified = false,$groups = null) { intval($remote_user), intval($owner_id) ); - if(count($r)) { + if (dbm::is_result($r)) { $remote_verified = true; $groups = init_groups_visitor($remote_user); } @@ -224,11 +224,11 @@ function permissions_sql($owner_id,$remote_verified = false,$groups = null) { if(is_array($groups) && count($groups)) { foreach($groups as $g) $gs .= '|<' . intval($g) . '>'; - } + } /*$sql = sprintf( - " AND ( allow_cid = '' OR allow_cid REGEXP '<%d>' ) - AND ( deny_cid = '' OR NOT deny_cid REGEXP '<%d>' ) + " AND ( allow_cid = '' OR allow_cid REGEXP '<%d>' ) + AND ( deny_cid = '' OR NOT deny_cid REGEXP '<%d>' ) AND ( allow_gid = '' OR allow_gid REGEXP '%s' ) AND ( deny_gid = '' OR NOT deny_gid REGEXP '%s') ", @@ -280,7 +280,7 @@ function item_permissions_sql($owner_id,$remote_verified = false,$groups = null) } /** - * Authenticated visitor. Unless pre-verified, + * Authenticated visitor. Unless pre-verified, * check that the contact belongs to this $owner_id * and load the groups the visitor belongs to. * If pre-verified, the caller is expected to have already @@ -294,7 +294,7 @@ function item_permissions_sql($owner_id,$remote_verified = false,$groups = null) intval($remote_user), intval($owner_id) ); - if(count($r)) { + if (dbm::is_result($r)) { $remote_verified = true; $groups = init_groups_visitor($remote_user); } @@ -306,13 +306,13 @@ function item_permissions_sql($owner_id,$remote_verified = false,$groups = null) if(is_array($groups) && count($groups)) { foreach($groups as $g) $gs .= '|<' . intval($g) . '>'; - } + } $sql = sprintf( - /*" AND ( private = 0 OR ( private in (1,2) AND wall = 1 AND ( allow_cid = '' OR allow_cid REGEXP '<%d>' ) - AND ( deny_cid = '' OR NOT deny_cid REGEXP '<%d>' ) + /*" AND ( private = 0 OR ( private in (1,2) AND wall = 1 AND ( allow_cid = '' OR allow_cid REGEXP '<%d>' ) + AND ( deny_cid = '' OR NOT deny_cid REGEXP '<%d>' ) AND ( allow_gid = '' OR allow_gid REGEXP '%s' ) - AND ( deny_gid = '' OR NOT deny_gid REGEXP '%s'))) + AND ( deny_gid = '' OR NOT deny_gid REGEXP '%s'))) ", intval($remote_user), intval($remote_user), @@ -345,29 +345,29 @@ function item_permissions_sql($owner_id,$remote_verified = false,$groups = null) * If the new page contains by any chance external elements, then the used security token is exposed by the referrer. * Actually, important actions should not be triggered by Links / GET-Requests at all, but somethimes they still are, * so this mechanism brings in some damage control (the attacker would be able to forge a request to a form of this type, but not to forms of other types). - */ + */ function get_form_security_token($typename = '') { $a = get_app(); - + $timestamp = time(); $sec_hash = hash('whirlpool', $a->user['guid'] . $a->user['prvkey'] . session_id() . $timestamp . $typename); - + return $timestamp . '.' . $sec_hash; } function check_form_security_token($typename = '', $formname = 'form_security_token') { if (!x($_REQUEST, $formname)) return false; $hash = $_REQUEST[$formname]; - + $max_livetime = 10800; // 3 hours - + $a = get_app(); - + $x = explode('.', $hash); if (time() > (IntVal($x[0]) + $max_livetime)) return false; - + $sec_hash = hash('whirlpool', $a->user['guid'] . $a->user['prvkey'] . session_id() . $x[0] . $typename); - + return ($sec_hash == $x[1]); } @@ -380,7 +380,7 @@ function check_form_security_token_redirectOnErr($err_redirect, $typename = '', logger('check_form_security_token failed: user ' . $a->user['guid'] . ' - form element ' . $typename); logger('check_form_security_token failed: _REQUEST data: ' . print_r($_REQUEST, true), LOGGER_DATA); notice( check_form_security_std_err_msg() ); - goaway($a->get_baseurl() . $err_redirect ); + goaway(App::get_baseurl() . $err_redirect ); } } function check_form_security_token_ForbiddenOnErr($typename = '', $formname = 'form_security_token') { @@ -395,17 +395,17 @@ function check_form_security_token_ForbiddenOnErr($typename = '', $formname = 'f // Returns an array of group id's this contact is a member of. // This array will only contain group id's related to the uid of this -// DFRN contact. They are *not* neccessarily unique across the entire site. +// DFRN contact. They are *not* neccessarily unique across the entire site. if(! function_exists('init_groups_visitor')) { function init_groups_visitor($contact_id) { $groups = array(); - $r = q("SELECT `gid` FROM `group_member` + $r = q("SELECT `gid` FROM `group_member` WHERE `contact-id` = %d ", intval($contact_id) ); - if(count($r)) { + if (dbm::is_result($r)) { foreach($r as $rr) $groups[] = $rr['gid']; } diff --git a/include/session.php b/include/session.php index 6632b7e89a..055bfcb4ef 100644 --- a/include/session.php +++ b/include/session.php @@ -1,70 +1,112 @@ get(get_app()->get_hostname().":session:".$id); + if (!is_bool($data)) { + return $data; + } + logger("no data for session $id", LOGGER_TRACE); + return ''; + } - if($session_exists) - $r = q("UPDATE `session` - SET `data` = '%s', `expire` = '%s' - WHERE `sid` = '%s'", - dbesc($data), dbesc($expire), dbesc($id)); - else - $r = q("INSERT INTO `session` - SET `sid` = '%s', `expire` = '%s', `data` = '%s'", - dbesc($id), dbesc($default_expire), dbesc($data)); + $r = q("SELECT `data` FROM `session` WHERE `sid`= '%s'", dbesc($id)); - return true; -}} + if (dbm::is_result($r)) { + $session_exists = true; + return $r[0]['data']; + } else { + logger("no data for session $id", LOGGER_TRACE); + } + + return ''; +} + +/** + * @brief Standard PHP session write callback + * + * This callback updates the DB-stored session data and/or the expiration depending + * on the case. Uses the $session_expire global for existing session, 5 minutes + * for newly created session. + * + * @global bool $session_exists Whether a session with the given id already exists + * @global int $session_expire Session expiration delay in seconds + * @param string $id Session ID with format: [a-z0-9]{26} + * @param string $data Serialized session data + * @return boolean Returns false if parameters are missing, true otherwise + */ +function ref_session_write($id, $data) { + global $session_exists, $session_expire; + + if (!$id || !$data) { + return false; + } + + $expire = time() + $session_expire; + $default_expire = time() + 300; + + $memcache = cache::memcache(); + $a = get_app(); + if (is_object($memcache) AND is_object($a)) { + $memcache->set($a->get_hostname().":session:".$id, $data, MEMCACHE_COMPRESSED, $expire); + return true; + } + + if ($session_exists) { + $r = q("UPDATE `session` + SET `data` = '%s', `expire` = '%s' + WHERE `sid` = '%s' + AND (`data` != '%s' OR `expire` != '%s')", + dbesc($data), dbesc($expire), dbesc($id), dbesc($data), dbesc($expire)); + } else { + $r = q("INSERT INTO `session` + SET `sid` = '%s', `expire` = '%s', `data` = '%s'", + dbesc($id), dbesc($default_expire), dbesc($data)); + } + + return true; +} -if(! function_exists('ref_session_close')) { function ref_session_close() { - return true; -}} + return true; +} -if(! function_exists('ref_session_destroy')) { -function ref_session_destroy ($id) { - q("DELETE FROM `session` WHERE `sid` = '%s'", dbesc($id)); - return true; -}} +function ref_session_destroy($id) { + $memcache = cache::memcache(); + + if (is_object($memcache)) { + $memcache->delete(get_app()->get_hostname().":session:".$id); + return true; + } + + q("DELETE FROM `session` WHERE `sid` = '%s'", dbesc($id)); + + return true; +} -if(! function_exists('ref_session_gc')) { function ref_session_gc($expire) { - q("DELETE FROM `session` WHERE `expire` < %d", dbesc(time())); - q("OPTIMIZE TABLE `sess_data`"); - return true; -}} + q("DELETE FROM `session` WHERE `expire` < %d", dbesc(time())); + + return true; +} $gc_probability = 50; @@ -72,7 +114,8 @@ ini_set('session.gc_probability', $gc_probability); ini_set('session.use_only_cookies', 1); ini_set('session.cookie_httponly', 1); - -session_set_save_handler ('ref_session_open', 'ref_session_close', - 'ref_session_read', 'ref_session_write', - 'ref_session_destroy', 'ref_session_gc'); +if (!get_config('system', 'disable_database_session')) { + session_set_save_handler('ref_session_open', 'ref_session_close', + 'ref_session_read', 'ref_session_write', + 'ref_session_destroy', 'ref_session_gc'); +} diff --git a/include/shadowupdate.php b/include/shadowupdate.php index 74c2a43ebd..83a785fe1f 100644 --- a/include/shadowupdate.php +++ b/include/shadowupdate.php @@ -1,4 +1,7 @@ displayName; - if(isset($entry->urls)) { - foreach($entry->urls as $url) { - if($url->type == 'profile') { + if (isset($entry->urls)) { + foreach ($entry->urls as $url) { + if ($url->type == 'profile') { $profile_url = $url->value; continue; } - if($url->type == 'webfinger') { + if ($url->type == 'webfinger') { $connect_url = str_replace('acct:' , '', $url->value); continue; } } } - if(isset($entry->photos)) { - foreach($entry->photos as $photo) { - if($photo->type == 'profile') { + if (isset($entry->photos)) { + foreach ($entry->photos as $photo) { + if ($photo->type == 'profile') { $profile_photo = $photo->value; continue; } } } - if(isset($entry->updated)) + if (isset($entry->updated)) { $updated = date("Y-m-d H:i:s", strtotime($entry->updated)); + } - if(isset($entry->network)) + if (isset($entry->network)) { $network = $entry->network; + } - if(isset($entry->currentLocation)) + if (isset($entry->currentLocation)) { $location = $entry->currentLocation; + } - if(isset($entry->aboutMe)) + if (isset($entry->aboutMe)) { $about = html2bbcode($entry->aboutMe); + } - if(isset($entry->gender)) + if (isset($entry->gender)) { $gender = $entry->gender; + } - if(isset($entry->generation) AND ($entry->generation > 0)) + if (isset($entry->generation) AND ($entry->generation > 0)) { $generation = ++$entry->generation; + } - if(isset($entry->tags)) - foreach($entry->tags as $tag) + if (isset($entry->tags)) { + foreach($entry->tags as $tag) { $keywords = implode(", ", $tag); + } + } + + if (isset($entry->contactType) AND ($entry->contactType >= 0)) + $contact_type = $entry->contactType; // If you query a Friendica server for its profiles, the network has to be Friendica - // To-Do: It could also be a Redmatrix server + /// TODO It could also be a Redmatrix server //if ($uid == 0) // $network = NETWORK_DFRN; poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, $cid, $uid, $zcid); + $gcontact = array("url" => $profile_url, "contact-type" => $contact_type, "generation" => $generation); + update_gcontact($gcontact); + // Update the Friendica contacts. Diaspora is doing it via a message. (See include/diaspora.php) - if (($location != "") OR ($about != "") OR ($keywords != "") OR ($gender != "")) - q("UPDATE `contact` SET `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s' - WHERE `nurl` = '%s' AND NOT `self` AND `network` = '%s'", - dbesc($location), - dbesc($about), - dbesc($keywords), - dbesc($gender), - dbesc(normalise_link($profile_url)), - dbesc(NETWORK_DFRN)); + // Deactivated because we now update Friendica contacts in dfrn.php + //if (($location != "") OR ($about != "") OR ($keywords != "") OR ($gender != "")) + // q("UPDATE `contact` SET `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s' + // WHERE `nurl` = '%s' AND NOT `self` AND `network` = '%s'", + // dbesc($location), + // dbesc($about), + // dbesc($keywords), + // dbesc($gender), + // dbesc(normalise_link($profile_url)), + // dbesc(NETWORK_DFRN)); } logger("poco_load: loaded $total entries",LOGGER_DEBUG); @@ -160,8 +179,6 @@ function poco_load($cid,$uid = 0,$zcid = 0,$url = null) { function poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, $cid = 0, $uid = 0, $zcid = 0) { - $a = get_app(); - // Generation: // 0: No definition // 1: Profiles on this server @@ -171,8 +188,6 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca $gcid = ""; - $alternate = poco_alternate_ostatus_url($profile_url); - if ($profile_url == "") return $gcid; @@ -184,28 +199,36 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca "identi.ca", "alpha.app.net"))) return $gcid; - $orig_updated = $updated; - // Don't store the statusnet connector as network // We can't simply set this to NETWORK_OSTATUS since the connector could have fetched posts from friendica as well if ($network == NETWORK_STATUSNET) $network = ""; + // Assure that there are no parameter fragments in the profile url + if (in_array($network, array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) + $profile_url = clean_contact_url($profile_url); + + $alternate = poco_alternate_ostatus_url($profile_url); + + $orig_updated = $updated; + // The global contacts should contain the original picture, not the cached one - if (($generation != 1) AND stristr(normalise_link($profile_photo), normalise_link($a->get_baseurl()."/photo/"))) + if (($generation != 1) AND stristr(normalise_link($profile_photo), normalise_link(App::get_baseurl()."/photo/"))) { $profile_photo = ""; + } $r = q("SELECT `network` FROM `contact` WHERE `nurl` = '%s' AND `network` != '' AND `network` != '%s' LIMIT 1", dbesc(normalise_link($profile_url)), dbesc(NETWORK_STATUSNET) ); - if(count($r)) + if (dbm::is_result($r)) { $network = $r[0]["network"]; + } if (($network == "") OR ($network == NETWORK_OSTATUS)) { $r = q("SELECT `network`, `url` FROM `contact` WHERE `alias` IN ('%s', '%s') AND `network` != '' AND `network` != '%s' LIMIT 1", dbesc($profile_url), dbesc(normalise_link($profile_url)), dbesc(NETWORK_STATUSNET) ); - if(count($r)) { + if (dbm::is_result($r)) { $network = $r[0]["network"]; //$profile_url = $r[0]["url"]; } @@ -226,6 +249,8 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca $server_url = $x[0]["server_url"]; $nick = $x[0]["nick"]; $addr = $x[0]["addr"]; + $alias = $x[0]["alias"]; + $notify = $x[0]["notify"]; } else { $created = "0000-00-00 00:00:00"; $server_url = ""; @@ -233,6 +258,8 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca $urlparts = parse_url($profile_url); $nick = end(explode("/", $urlparts["path"])); $addr = ""; + $alias = ""; + $notify = ""; } if ((($network == "") OR ($name == "") OR ($addr == "") OR ($profile_photo == "") OR ($server_url == "") OR $alternate) @@ -245,6 +272,8 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca $name = $data["name"]; $nick = $data["nick"]; $addr = $data["addr"]; + $alias = $data["alias"]; + $notify = $data["notify"]; $profile_url = $data["url"]; $profile_photo = $data["photo"]; $server_url = $data["baseurl"]; @@ -282,76 +311,25 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca poco_check_server($server_url, $network); - if(count($x)) { - $gcid = $x[0]['id']; + $gcontact = array("url" => $profile_url, + "addr" => $addr, + "alias" => $alias, + "name" => $name, + "network" => $network, + "photo" => $profile_photo, + "about" => $about, + "location" => $location, + "gender" => $gender, + "keywords" => $keywords, + "server_url" => $server_url, + "connect" => $connect_url, + "notify" => $notify, + "updated" => $updated, + "generation" => $generation); - if (($location == "") AND ($x[0]['location'] != "")) - $location = $x[0]['location']; + $gcid = update_gcontact($gcontact); - if (($about == "") AND ($x[0]['about'] != "")) - $about = $x[0]['about']; - - if (($gender == "") AND ($x[0]['gender'] != "")) - $gender = $x[0]['gender']; - - if (($keywords == "") AND ($x[0]['keywords'] != "")) - $keywords = $x[0]['keywords']; - - if (($addr == "") AND ($x[0]['addr'] != "")) - $addr = $x[0]['addr']; - - if (($generation == 0) AND ($x[0]['generation'] > 0)) - $generation = $x[0]['generation']; - - if($x[0]['name'] != $name || $x[0]['photo'] != $profile_photo || $x[0]['updated'] < $updated) { - q("UPDATE `gcontact` SET `name` = '%s', `addr` = '%s', `network` = '%s', `photo` = '%s', `connect` = '%s', `url` = '%s', `server_url` = '%s', - `updated` = '%s', `location` = '%s', `about` = '%s', `keywords` = '%s', `gender` = '%s', `generation` = %d - WHERE (`generation` >= %d OR `generation` = 0) AND `nurl` = '%s'", - dbesc($name), - dbesc($addr), - dbesc($network), - dbesc($profile_photo), - dbesc($connect_url), - dbesc($profile_url), - dbesc($server_url), - dbesc($updated), - dbesc($location), - dbesc($about), - dbesc($keywords), - dbesc($gender), - intval($generation), - intval($generation), - dbesc(normalise_link($profile_url)) - ); - } - } else { - q("INSERT INTO `gcontact` (`name`, `nick`, `addr`, `network`, `url`, `nurl`, `photo`, `connect`, `server_url`, `created`, `updated`, `location`, `about`, `keywords`, `gender`, `generation`) - VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d)", - dbesc($name), - dbesc($nick), - dbesc($addr), - dbesc($network), - dbesc($profile_url), - dbesc(normalise_link($profile_url)), - dbesc($profile_photo), - dbesc($connect_url), - dbesc($server_url), - dbesc(datetime_convert()), - dbesc($updated), - dbesc($location), - dbesc($about), - dbesc($keywords), - dbesc($gender), - intval($generation) - ); - $x = q("SELECT * FROM `gcontact` WHERE `nurl` = '%s' LIMIT 1", - dbesc(normalise_link($profile_url)) - ); - if(count($x)) - $gcid = $x[0]['id']; - } - - if(! $gcid) + if(!$gcid) return $gcid; $r = q("SELECT * FROM `glink` WHERE `cid` = %d AND `uid` = %d AND `gcid` = %d AND `zcid` = %d LIMIT 1", @@ -360,7 +338,7 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca intval($gcid), intval($zcid) ); - if(! count($r)) { + if (! dbm::is_result($r)) { q("INSERT INTO `glink` (`cid`,`uid`,`gcid`,`zcid`, `updated`) VALUES (%d,%d,%d,%d, '%s') ", intval($cid), intval($uid), @@ -378,13 +356,6 @@ function poco_check($profile_url, $name, $network, $profile_photo, $about, $loca ); } - // For unknown reasons there are sometimes duplicates - q("DELETE FROM `gcontact` WHERE `nurl` = '%s' AND `id` != %d AND - NOT EXISTS (SELECT `gcid` FROM `glink` WHERE `gcid` = `gcontact`.`id`)", - dbesc(normalise_link($profile_url)), - intval($gcid) - ); - return $gcid; } @@ -428,6 +399,15 @@ function poco_detect_server($profile) { } } + // Mastodon + if ($server_url == "") { + $red = preg_replace("=(https?://)(.*)/users/(.*)=ism", "$1$2", $profile); + if ($red != $profile) { + $server_url = $red; + $network = NETWORK_OSTATUS; + } + } + return $server_url; } @@ -449,6 +429,11 @@ function poco_last_updated($profile, $force = false) { else $server_url = poco_detect_server($profile); + if (!in_array($gcontacts[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_FEED, NETWORK_OSTATUS, ""))) { + logger("Profile ".$profile.": Network type ".$gcontacts[0]["network"]." can't be checked", LOGGER_DEBUG); + return false; + } + if ($server_url != "") { if (!poco_check_server($server_url, $gcontacts[0]["network"], $force)) { @@ -456,6 +441,7 @@ function poco_last_updated($profile, $force = false) { q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc(normalise_link($profile))); + logger("Profile ".$profile.": Server ".$server_url." wasn't reachable.", LOGGER_DEBUG); return false; } @@ -471,14 +457,14 @@ function poco_last_updated($profile, $force = false) { q("UPDATE `gcontact` SET `network` = '%s' WHERE `nurl` = '%s'", dbesc($server[0]["network"]), dbesc(normalise_link($profile))); else - return; + return false; } // noscrape is really fast so we don't cache the call. if (($gcontacts[0]["server_url"] != "") AND ($gcontacts[0]["nick"] != "")) { // Use noscrape if possible - $server = q("SELECT `noscrape` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", dbesc(normalise_link($gcontacts[0]["server_url"]))); + $server = q("SELECT `noscrape`, `network` FROM `gserver` WHERE `nurl` = '%s' AND `noscrape` != ''", dbesc(normalise_link($gcontacts[0]["server_url"]))); if ($server) { $noscraperet = z_fetch_url($server[0]["noscrape"]."/".$gcontacts[0]["nick"]); @@ -487,76 +473,71 @@ function poco_last_updated($profile, $force = false) { $noscrape = json_decode($noscraperet["body"], true); - if (($noscrape["fn"] != "") AND ($noscrape["fn"] != $gcontacts[0]["name"])) - q("UPDATE `gcontact` SET `name` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["fn"]), dbesc(normalise_link($profile))); + if (is_array($noscrape)) { + $contact = array("url" => $profile, + "network" => $server[0]["network"], + "generation" => $gcontacts[0]["generation"]); - if (($noscrape["photo"] != "") AND ($noscrape["photo"] != $gcontacts[0]["photo"])) - q("UPDATE `gcontact` SET `photo` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["photo"]), dbesc(normalise_link($profile))); + if (isset($noscrape["fn"])) + $contact["name"] = $noscrape["fn"]; - if (($noscrape["updated"] != "") AND ($noscrape["updated"] != $gcontacts[0]["updated"])) - q("UPDATE `gcontact` SET `updated` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["updated"]), dbesc(normalise_link($profile))); + if (isset($noscrape["comm"])) + $contact["community"] = $noscrape["comm"]; - if (($noscrape["gender"] != "") AND ($noscrape["gender"] != $gcontacts[0]["gender"])) - q("UPDATE `gcontact` SET `gender` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["gender"]), dbesc(normalise_link($profile))); + if (isset($noscrape["tags"])) { + $keywords = implode(" ", $noscrape["tags"]); + if ($keywords != "") + $contact["keywords"] = $keywords; + } - if (($noscrape["pdesc"] != "") AND ($noscrape["pdesc"] != $gcontacts[0]["about"])) - q("UPDATE `gcontact` SET `about` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["pdesc"]), dbesc(normalise_link($profile))); + $location = formatted_location($noscrape); + if ($location) + $contact["location"] = $location; - if (($noscrape["about"] != "") AND ($noscrape["about"] != $gcontacts[0]["about"])) - q("UPDATE `gcontact` SET `about` = '%s' WHERE `nurl` = '%s'", - dbesc($noscrape["about"]), dbesc(normalise_link($profile))); + if (isset($noscrape["dfrn-notify"])) + $contact["notify"] = $noscrape["dfrn-notify"]; - if (isset($noscrape["comm"]) AND ($noscrape["comm"] != $gcontacts[0]["community"])) - q("UPDATE `gcontact` SET `community` = %d WHERE `nurl` = '%s'", - intval($noscrape["comm"]), dbesc(normalise_link($profile))); + // Remove all fields that are not present in the gcontact table + unset($noscrape["fn"]); + unset($noscrape["key"]); + unset($noscrape["homepage"]); + unset($noscrape["comm"]); + unset($noscrape["tags"]); + unset($noscrape["locality"]); + unset($noscrape["region"]); + unset($noscrape["country-name"]); + unset($noscrape["contacts"]); + unset($noscrape["dfrn-request"]); + unset($noscrape["dfrn-confirm"]); + unset($noscrape["dfrn-notify"]); + unset($noscrape["dfrn-poll"]); - if (isset($noscrape["tags"])) - $keywords = implode(" ", $noscrape["tags"]); - else - $keywords = ""; + // Set the date of the last contact + /// @todo By now the function "update_gcontact" doesn't work with this field + //$contact["last_contact"] = datetime_convert(); - if (($keywords != "") AND ($keywords != $gcontacts[0]["keywords"])) - q("UPDATE `gcontact` SET `keywords` = '%s' WHERE `nurl` = '%s'", - dbesc($keywords), dbesc(normalise_link($profile))); + $contact = array_merge($contact, $noscrape); - $location = $noscrape["locality"]; + update_gcontact($contact); - if ($noscrape["region"] != "") { - if ($location != "") - $location .= ", "; + if (trim($noscrape["updated"]) != "") { + q("UPDATE `gcontact` SET `last_contact` = '%s' WHERE `nurl` = '%s'", + dbesc(datetime_convert()), dbesc(normalise_link($profile))); - $location .= $noscrape["region"]; + logger("Profile ".$profile." was last updated at ".$noscrape["updated"]." (noscrape)", LOGGER_DEBUG); + + return $noscrape["updated"]; + } } - - if ($noscrape["country-name"] != "") { - if ($location != "") - $location .= ", "; - - $location .= $noscrape["country-name"]; - } - - if (($location != "") AND ($location != $gcontacts[0]["location"])) - q("UPDATE `gcontact` SET `location` = '%s' WHERE `nurl` = '%s'", - dbesc($location), dbesc(normalise_link($profile))); - - // If we got data from noscrape then mark the contact as reachable - if (is_array($noscrape) AND count($noscrape)) - q("UPDATE `gcontact` SET `last_contact` = '%s' WHERE `nurl` = '%s'", - dbesc(datetime_convert()), dbesc(normalise_link($profile))); - - return $noscrape["updated"]; } } } // If we only can poll the feed, then we only do this once a while - if (!$force AND !poco_do_update($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) + if (!$force AND !poco_do_update($gcontacts[0]["created"], $gcontacts[0]["updated"], $gcontacts[0]["last_failure"], $gcontacts[0]["last_contact"])) { + logger("Profile ".$profile." was last updated at ".$gcontacts[0]["updated"]." (cached)", LOGGER_DEBUG); return $gcontacts[0]["updated"]; + } $data = probe_url($profile); @@ -575,40 +556,42 @@ function poco_last_updated($profile, $force = false) { poco_last_updated($data["url"], $force); + logger("Profile ".$profile." was deleted", LOGGER_DEBUG); return false; } if (($data["poll"] == "") OR (in_array($data["network"], array(NETWORK_FEED, NETWORK_PHANTOM)))) { q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc(normalise_link($profile))); + + logger("Profile ".$profile." wasn't reachable (profile)", LOGGER_DEBUG); return false; } - if (($data["name"] != "") AND ($data["name"] != $gcontacts[0]["name"])) - q("UPDATE `gcontact` SET `name` = '%s' WHERE `nurl` = '%s'", - dbesc($data["name"]), dbesc(normalise_link($profile))); + $contact = array("generation" => $gcontacts[0]["generation"]); - if (($data["nick"] != "") AND ($data["nick"] != $gcontacts[0]["nick"])) - q("UPDATE `gcontact` SET `nick` = '%s' WHERE `nurl` = '%s'", - dbesc($data["nick"]), dbesc(normalise_link($profile))); + $contact = array_merge($contact, $data); - if (($data["addr"] != "") AND ($data["addr"] != $gcontacts[0]["connect"])) - q("UPDATE `gcontact` SET `connect` = '%s' WHERE `nurl` = '%s'", - dbesc($data["addr"]), dbesc(normalise_link($profile))); + $contact["server_url"] = $data["baseurl"]; - if (($data["photo"] != "") AND ($data["photo"] != $gcontacts[0]["photo"])) - q("UPDATE `gcontact` SET `photo` = '%s' WHERE `nurl` = '%s'", - dbesc($data["photo"]), dbesc(normalise_link($profile))); + unset($contact["batch"]); + unset($contact["poll"]); + unset($contact["request"]); + unset($contact["confirm"]); + unset($contact["poco"]); + unset($contact["priority"]); + unset($contact["pubkey"]); + unset($contact["baseurl"]); - if (($data["baseurl"] != "") AND ($data["baseurl"] != $gcontacts[0]["server_url"])) - q("UPDATE `gcontact` SET `server_url` = '%s' WHERE `nurl` = '%s'", - dbesc($data["baseurl"]), dbesc(normalise_link($profile))); + update_gcontact($contact); $feedret = z_fetch_url($data["poll"]); if (!$feedret["success"]) { q("UPDATE `gcontact` SET `last_failure` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc(normalise_link($profile))); + + logger("Profile ".$profile." wasn't reachable (no feed)", LOGGER_DEBUG); return false; } @@ -639,12 +622,14 @@ function poco_last_updated($profile, $force = false) { $last_updated = "0000-00-00 00:00:00"; q("UPDATE `gcontact` SET `updated` = '%s', `last_contact` = '%s' WHERE `nurl` = '%s'", - dbesc($last_updated), dbesc(datetime_convert()), dbesc(normalise_link($profile))); + dbesc(dbm::date($last_updated)), dbesc(dbm::date()), dbesc(normalise_link($profile))); if (($gcontacts[0]["generation"] == 0)) q("UPDATE `gcontact` SET `generation` = 9 WHERE `nurl` = '%s'", dbesc(normalise_link($profile))); + logger("Profile ".$profile." was last updated at ".$last_updated, LOGGER_DEBUG); + return($last_updated); } @@ -697,6 +682,10 @@ function poco_to_boolean($val) { function poco_check_server($server_url, $network = "", $force = false) { + // Unify the server address + $server_url = trim($server_url, "/"); + $server_url = str_replace("/index.php", "", $server_url); + if ($server_url == "") return false; @@ -768,17 +757,30 @@ function poco_check_server($server_url, $network = "", $force = false) { // Test for Diaspora $serverret = z_fetch_url($server_url); - $lines = explode("\n",$serverret["header"]); - if(count($lines)) - foreach($lines as $line) { - $line = trim($line); - if(stristr($line,'X-Diaspora-Version:')) { - $platform = "Diaspora"; - $version = trim(str_replace("X-Diaspora-Version:", "", $line)); - $version = trim(str_replace("x-diaspora-version:", "", $version)); - $network = NETWORK_DIASPORA; + if (!$serverret["success"] OR ($serverret["body"] == "")) + $failure = true; + else { + $lines = explode("\n",$serverret["header"]); + if(count($lines)) + foreach($lines as $line) { + $line = trim($line); + if(stristr($line,'X-Diaspora-Version:')) { + $platform = "Diaspora"; + $version = trim(str_replace("X-Diaspora-Version:", "", $line)); + $version = trim(str_replace("x-diaspora-version:", "", $version)); + $network = NETWORK_DIASPORA; + $versionparts = explode("-", $version); + $version = $versionparts[0]; + } + + if(stristr($line,'Server: Mastodon')) { + $platform = "Mastodon"; + $network = NETWORK_OSTATUS; + // Mastodon doesn't reveal version numbers + $version = ""; + } } - } + } } if (!$failure) { @@ -786,7 +788,8 @@ function poco_check_server($server_url, $network = "", $force = false) { // Will also return data for Friendica and GNU Social - but it will be overwritten later // The "not implemented" is a special treatment for really, really old Friendica versions $serverret = z_fetch_url($server_url."/api/statusnet/version.json"); - if ($serverret["success"] AND ($serverret["body"] != '{"error":"not implemented"}') AND ($serverret["body"] != '') AND (strlen($serverret["body"]) < 250)) { + if ($serverret["success"] AND ($serverret["body"] != '{"error":"not implemented"}') AND + ($serverret["body"] != '') AND (strlen($serverret["body"]) < 30)) { $platform = "StatusNet"; $version = trim($serverret["body"], '"'); $network = NETWORK_OSTATUS; @@ -794,7 +797,8 @@ function poco_check_server($server_url, $network = "", $force = false) { // Test for GNU Social $serverret = z_fetch_url($server_url."/api/gnusocial/version.json"); - if ($serverret["success"] AND ($serverret["body"] != '{"error":"not implemented"}') AND ($serverret["body"] != '') AND (strlen($serverret["body"]) < 250)) { + if ($serverret["success"] AND ($serverret["body"] != '{"error":"not implemented"}') AND + ($serverret["body"] != '') AND (strlen($serverret["body"]) < 30)) { $platform = "GNU Social"; $version = trim($serverret["body"], '"'); $network = NETWORK_OSTATUS; @@ -921,6 +925,11 @@ function poco_check_server($server_url, $network = "", $force = false) { // Check again if the server exists $servers = q("SELECT `nurl` FROM `gserver` WHERE `nurl` = '%s'", dbesc(normalise_link($server_url))); + $version = strip_tags($version); + $site_name = strip_tags($site_name); + $info = strip_tags($info); + $platform = strip_tags($platform); + if ($servers) q("UPDATE `gserver` SET `url` = '%s', `version` = '%s', `site_name` = '%s', `info` = '%s', `register_policy` = %d, `poco` = '%s', `noscrape` = '%s', `network` = '%s', `platform` = '%s', `last_contact` = '%s', `last_failure` = '%s' WHERE `nurl` = '%s'", @@ -961,88 +970,6 @@ function poco_check_server($server_url, $network = "", $force = false) { return !$failure; } -function poco_contact_from_body($body, $created, $cid, $uid) { - preg_replace_callback("/\[share(.*?)\].*?\[\/share\]/ism", - function ($match) use ($created, $cid, $uid){ - return(sub_poco_from_share($match, $created, $cid, $uid)); - }, $body); -} - -function sub_poco_from_share($share, $created, $cid, $uid) { - $profile = ""; - preg_match("/profile='(.*?)'/ism", $share[1], $matches); - if ($matches[1] != "") - $profile = $matches[1]; - - preg_match('/profile="(.*?)"/ism', $share[1], $matches); - if ($matches[1] != "") - $profile = $matches[1]; - - if ($profile == "") - return; - - logger("prepare poco_check for profile ".$profile, LOGGER_DEBUG); - poco_check($profile, "", "", "", "", "", "", "", "", $created, 3, $cid, $uid); -} - -function poco_store($item) { - - // Isn't it public? - if ($item['private']) - return; - - // Or is it from a network where we don't store the global contacts? - if (!in_array($item["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, NETWORK_STATUSNET, ""))) - return; - - // Is it a global copy? - $store_gcontact = ($item["uid"] == 0); - - // Is it a comment on a global copy? - if (!$store_gcontact AND ($item["uri"] != $item["parent-uri"])) { - $q = q("SELECT `id` FROM `item` WHERE `uri`='%s' AND `uid` = 0", $item["parent-uri"]); - $store_gcontact = count($q); - } - - if (!$store_gcontact) - return; - - // "3" means: We don't know this contact directly (Maybe a reshared item) - $generation = 3; - $network = ""; - $profile_url = $item["author-link"]; - - // Is it a user from our server? - $q = q("SELECT `id` FROM `contact` WHERE `self` AND `nurl` = '%s' LIMIT 1", - dbesc(normalise_link($item["author-link"]))); - if (count($q)) { - logger("Our user (generation 1): ".$item["author-link"], LOGGER_DEBUG); - $generation = 1; - $network = NETWORK_DFRN; - } else { // Is it a contact from a user on our server? - $q = q("SELECT `network`, `url` FROM `contact` WHERE `uid` != 0 AND `network` != '' - AND (`nurl` = '%s' OR `alias` IN ('%s', '%s')) AND `network` != '%s' LIMIT 1", - dbesc(normalise_link($item["author-link"])), - dbesc(normalise_link($item["author-link"])), - dbesc($item["author-link"]), - dbesc(NETWORK_STATUSNET)); - if (count($q)) { - $generation = 2; - $network = $q[0]["network"]; - $profile_url = $q[0]["url"]; - logger("Known contact (generation 2): ".$profile_url, LOGGER_DEBUG); - } - } - - if ($generation == 3) - logger("Unknown contact (generation 3): ".$item["author-link"], LOGGER_DEBUG); - - poco_check($profile_url, $item["author-name"], $network, $item["author-avatar"], "", "", "", "", "", $item["received"], $generation, $item["contact-id"], $item["uid"]); - - // Maybe its a body with a shared item? Then extract a global contact from it. - poco_contact_from_body($item["body"], $item["received"], $item["contact-id"], $item["uid"]); -} - function count_common_friends($uid,$cid) { $r = q("SELECT count(*) as `total` @@ -1057,7 +984,7 @@ function count_common_friends($uid,$cid) { ); // logger("count_common_friends: $uid $cid {$r[0]['total']}"); - if(count($r)) + if (dbm::is_result($r)) return $r[0]['total']; return 0; @@ -1103,7 +1030,7 @@ function count_common_friends_zcid($uid,$zcid) { intval($uid) ); - if(count($r)) + if (dbm::is_result($r)) return $r[0]['total']; return 0; @@ -1142,7 +1069,7 @@ function count_all_friends($uid,$cid) { intval($uid) ); - if(count($r)) + if (dbm::is_result($r)) return $r[0]['total']; return 0; @@ -1172,8 +1099,16 @@ function all_friends($uid,$cid,$start = 0, $limit = 80) { function suggestion_query($uid, $start = 0, $limit = 80) { - if(! $uid) + if (!$uid) { return array(); + } + +// Uncommented because the result of the queries are to big to store it in the cache. +// We need to decide if we want to change the db column type or if we want to delete it. +// $list = Cache::get("suggestion_query:".$uid.":".$start.":".$limit); +// if (!is_null($list)) { +// return $list; +// } $network = array(NETWORK_DFRN); @@ -1184,9 +1119,10 @@ function suggestion_query($uid, $start = 0, $limit = 80) { $network[] = NETWORK_OSTATUS; $sql_network = implode("', '", $network); - //$sql_network = "'".$sql_network."', ''"; $sql_network = "'".$sql_network."'"; + /// @todo This query is really slow + // By now we cache the data for five minutes $r = q("SELECT count(glink.gcid) as `total`, gcontact.* from gcontact INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id` where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d ) @@ -1205,8 +1141,13 @@ function suggestion_query($uid, $start = 0, $limit = 80) { intval($limit) ); - if(count($r) && count($r) >= ($limit -1)) + if (dbm::is_result($r) && count($r) >= ($limit -1)) { +// Uncommented because the result of the queries are to big to store it in the cache. +// We need to decide if we want to change the db column type or if we want to delete it. +// Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $r, CACHE_FIVE_MINUTES); + return $r; + } $r2 = q("SELECT gcontact.* FROM gcontact INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id` @@ -1235,6 +1176,9 @@ function suggestion_query($uid, $start = 0, $limit = 80) { while (sizeof($list) > ($limit)) array_pop($list); +// Uncommented because the result of the queries are to big to store it in the cache. +// We need to decide if we want to change the db column type or if we want to delete it. +// Cache::set("suggestion_query:".$uid.":".$start.":".$limit, $list, CACHE_FIVE_MINUTES); return $list; } @@ -1244,23 +1188,24 @@ function update_suggestions() { $done = array(); - // To-Do: Check if it is really neccessary to poll the own server - poco_load(0,0,0,$a->get_baseurl() . '/poco'); + /// @TODO Check if it is really neccessary to poll the own server + poco_load(0,0,0,App::get_baseurl() . '/poco'); - $done[] = $a->get_baseurl() . '/poco'; + $done[] = App::get_baseurl() . '/poco'; - if(strlen(get_config('system','directory'))) { + if (strlen(get_config('system','directory'))) { $x = fetch_url(get_server()."/pubsites"); - if($x) { + if ($x) { $j = json_decode($x); - if($j->entries) { - foreach($j->entries as $entry) { + if ($j->entries) { + foreach ($j->entries as $entry) { poco_check_server($entry->url); $url = $entry->url . '/poco'; - if(! in_array($url,$done)) + if (! in_array($url,$done)) { poco_load(0,0,0,$entry->url . '/poco'); + } } } } @@ -1271,8 +1216,8 @@ function update_suggestions() { dbesc(NETWORK_DFRN), dbesc(NETWORK_DIASPORA) ); - if(count($r)) { - foreach($r as $rr) { + if (dbm::is_result($r)) { + foreach ($r as $rr) { $base = substr($rr['poco'],0,strrpos($rr['poco'],'/')); if(! in_array($base,$done)) poco_load(0,0,0,$base); @@ -1283,24 +1228,38 @@ function update_suggestions() { function poco_discover_federation() { $last = get_config('poco','last_federation_discovery'); - if($last) { + if ($last) { $next = $last + (24 * 60 * 60); if($next > time()) return; } + // Discover Friendica, Hubzilla and Diaspora servers $serverdata = fetch_url("http://the-federation.info/pods.json"); - if (!$serverdata) - return; + if ($serverdata) { + $servers = json_decode($serverdata); - $servers = json_decode($serverdata); + foreach($servers->pods AS $server) + poco_check_server("https://".$server->host); + } - foreach($servers->pods AS $server) - poco_check_server("https://".$server->host); + // Currently disabled, since the service isn't available anymore. + // It is not removed since I hope that there will be a successor. + // Discover GNU Social Servers. + //if (!get_config('system','ostatus_disabled')) { + // $serverdata = "http://gstools.org/api/get_open_instances/"; + + // $result = z_fetch_url($serverdata); + // if ($result["success"]) { + // $servers = json_decode($result["body"]); + + // foreach($servers->data AS $server) + // poco_check_server($server->instance_address); + // } + //} set_config('poco','last_federation_discovery', time()); - } function poco_discover($complete = false) { @@ -1328,7 +1287,7 @@ function poco_discover($complete = false) { } // Fetch all users from the other server - $url = $server["poco"]."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,generation"; + $url = $server["poco"]."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation"; logger("Fetch all users from the server ".$server["nurl"], LOGGER_DEBUG); @@ -1347,7 +1306,7 @@ function poco_discover($complete = false) { $updatedSince = date("Y-m-d H:i:s", time() - $timeframe * 86400); // Fetch all global contacts from the other server (Not working with Redmatrix and Friendica versions before 3.3) - $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,generation"; + $url = $server["poco"]."/@global?updatedSince=".$updatedSince."&fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation"; $success = false; @@ -1385,7 +1344,7 @@ function poco_discover_server_users($data, $server) { $username = ""; if (isset($entry->urls)) { foreach($entry->urls as $url) - if($url->type == 'profile') { + if ($url->type == 'profile') { $profile_url = $url->value; $urlparts = parse_url($profile_url); $username = end(explode("/", $urlparts["path"])); @@ -1395,7 +1354,7 @@ function poco_discover_server_users($data, $server) { logger("Fetch contacts for the user ".$username." from the server ".$server["nurl"], LOGGER_DEBUG); // Fetch all contacts from a given user from the other server - $url = $server["poco"]."/".$username."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,generation"; + $url = $server["poco"]."/".$username."/?fields=displayName,urls,photos,updated,network,aboutMe,currentLocation,tags,gender,contactType,generation"; $retdata = z_fetch_url($url); if ($retdata["success"]) @@ -1422,62 +1381,483 @@ function poco_discover_server($data, $default_generation = 0) { $about = ''; $keywords = ''; $gender = ''; + $contact_type = -1; $generation = $default_generation; $name = $entry->displayName; - if(isset($entry->urls)) { + if (isset($entry->urls)) { foreach($entry->urls as $url) { - if($url->type == 'profile') { + if ($url->type == 'profile') { $profile_url = $url->value; continue; } - if($url->type == 'webfinger') { + if ($url->type == 'webfinger') { $connect_url = str_replace('acct:' , '', $url->value); continue; } } } - if(isset($entry->photos)) { - foreach($entry->photos as $photo) { - if($photo->type == 'profile') { + if (isset($entry->photos)) { + foreach ($entry->photos as $photo) { + if ($photo->type == 'profile') { $profile_photo = $photo->value; continue; } } } - if(isset($entry->updated)) + if (isset($entry->updated)) { $updated = date("Y-m-d H:i:s", strtotime($entry->updated)); + } - if(isset($entry->network)) + if(isset($entry->network)) { $network = $entry->network; + } - if(isset($entry->currentLocation)) + if(isset($entry->currentLocation)) { $location = $entry->currentLocation; + } - if(isset($entry->aboutMe)) + if(isset($entry->aboutMe)) { $about = html2bbcode($entry->aboutMe); + } - if(isset($entry->gender)) + if(isset($entry->gender)) { $gender = $entry->gender; + } - if(isset($entry->generation) AND ($entry->generation > 0)) + if(isset($entry->generation) AND ($entry->generation > 0)) { $generation = ++$entry->generation; + } - if(isset($entry->tags)) - foreach($entry->tags as $tag) + if(isset($entry->contactType) AND ($entry->contactType >= 0)) { + $contact_type = $entry->contactType; + } + + if(isset($entry->tags)) { + foreach ($entry->tags as $tag) { $keywords = implode(", ", $tag); + } + } if ($generation > 0) { $success = true; logger("Store profile ".$profile_url, LOGGER_DEBUG); poco_check($profile_url, $name, $network, $profile_photo, $about, $location, $gender, $keywords, $connect_url, $updated, $generation, 0, 0, 0); + + $gcontact = array("url" => $profile_url, "contact-type" => $contact_type, "generation" => $generation); + update_gcontact($gcontact); + logger("Done for profile ".$profile_url, LOGGER_DEBUG); } } return $success; } + +/** + * @brief Removes unwanted parts from a contact url + * + * @param string $url Contact url + * @return string Contact url with the wanted parts + */ +function clean_contact_url($url) { + $parts = parse_url($url); + + if (!isset($parts["scheme"]) OR !isset($parts["host"])) + return $url; + + $new_url = $parts["scheme"]."://".$parts["host"]; + + if (isset($parts["port"])) + $new_url .= ":".$parts["port"]; + + if (isset($parts["path"])) + $new_url .= $parts["path"]; + + if ($new_url != $url) + logger("Cleaned contact url ".$url." to ".$new_url." - Called by: ".App::callstack(), LOGGER_DEBUG); + + return $new_url; +} + +/** + * @brief Replace alternate OStatus user format with the primary one + * + * @param arr $contact contact array (called by reference) + */ +function fix_alternate_contact_address(&$contact) { + if (($contact["network"] == NETWORK_OSTATUS) AND poco_alternate_ostatus_url($contact["url"])) { + $data = probe_url($contact["url"]); + if ($contact["network"] == NETWORK_OSTATUS) { + logger("Fix primary url from ".$contact["url"]." to ".$data["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG); + $contact["url"] = $data["url"]; + $contact["addr"] = $data["addr"]; + $contact["alias"] = $data["alias"]; + $contact["server_url"] = $data["baseurl"]; + } + } +} + +/** + * @brief Fetch the gcontact id, add an entry if not existed + * + * @param arr $contact contact array + * @return bool|int Returns false if not found, integer if contact was found + */ +function get_gcontact_id($contact) { + + $gcontact_id = 0; + $doprobing = false; + + if (in_array($contact["network"], array(NETWORK_PHANTOM))) { + logger("Invalid network for contact url ".$contact["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG); + return false; + } + + if ($contact["network"] == NETWORK_STATUSNET) + $contact["network"] = NETWORK_OSTATUS; + + // All new contacts are hidden by default + if (!isset($contact["hide"])) + $contact["hide"] = true; + + // Replace alternate OStatus user format with the primary one + fix_alternate_contact_address($contact); + + // Remove unwanted parts from the contact url (e.g. "?zrl=...") + if (in_array($contact["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS))) + $contact["url"] = clean_contact_url($contact["url"]); + + $r = q("SELECT `id`, `last_contact`, `last_failure`, `network` FROM `gcontact` WHERE `nurl` = '%s' LIMIT 2", + dbesc(normalise_link($contact["url"]))); + + if ($r) { + $gcontact_id = $r[0]["id"]; + + // Update every 90 days + if (in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, ""))) { + $last_failure_str = $r[0]["last_failure"]; + $last_failure = strtotime($r[0]["last_failure"]); + $last_contact_str = $r[0]["last_contact"]; + $last_contact = strtotime($r[0]["last_contact"]); + $doprobing = (((time() - $last_contact) > (90 * 86400)) AND ((time() - $last_failure) > (90 * 86400))); + } + } else { + q("INSERT INTO `gcontact` (`name`, `nick`, `addr` , `network`, `url`, `nurl`, `photo`, `created`, `updated`, `location`, `about`, `hide`, `generation`) + VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d)", + dbesc($contact["name"]), + dbesc($contact["nick"]), + dbesc($contact["addr"]), + dbesc($contact["network"]), + dbesc($contact["url"]), + dbesc(normalise_link($contact["url"])), + dbesc($contact["photo"]), + dbesc(datetime_convert()), + dbesc(datetime_convert()), + dbesc($contact["location"]), + dbesc($contact["about"]), + intval($contact["hide"]), + intval($contact["generation"]) + ); + + $r = q("SELECT `id`, `network` FROM `gcontact` WHERE `nurl` = '%s' ORDER BY `id` LIMIT 2", + dbesc(normalise_link($contact["url"]))); + + if ($r) { + $gcontact_id = $r[0]["id"]; + + $doprobing = in_array($r[0]["network"], array(NETWORK_DFRN, NETWORK_DIASPORA, NETWORK_OSTATUS, "")); + } + } + + if ($doprobing) { + logger("Last Contact: ". $last_contact_str." - Last Failure: ".$last_failure_str." - Checking: ".$contact["url"], LOGGER_DEBUG); + proc_run(PRIORITY_LOW, 'include/gprobe.php', bin2hex($contact["url"])); + } + + if ((dbm::is_result($r)) AND (count($r) > 1) AND ($gcontact_id > 0) AND ($contact["url"] != "")) + q("DELETE FROM `gcontact` WHERE `nurl` = '%s' AND `id` != %d", + dbesc(normalise_link($contact["url"])), + intval($gcontact_id)); + + return $gcontact_id; +} + +/** + * @brief Updates the gcontact table from a given array + * + * @param arr $contact contact array + * @return bool|int Returns false if not found, integer if contact was found + */ +function update_gcontact($contact) { + + // Check for invalid "contact-type" value + if (isset($contact['contact-type']) AND (intval($contact['contact-type']) < 0)) { + $contact['contact-type'] = 0; + } + + /// @todo update contact table as well + + $gcontact_id = get_gcontact_id($contact); + + if (!$gcontact_id) + return false; + + $r = q("SELECT `name`, `nick`, `photo`, `location`, `about`, `addr`, `generation`, `birthday`, `gender`, `keywords`, + `contact-type`, `hide`, `nsfw`, `network`, `alias`, `notify`, `server_url`, `connect`, `updated`, `url` + FROM `gcontact` WHERE `id` = %d LIMIT 1", + intval($gcontact_id)); + + // Get all field names + $fields = array(); + foreach ($r[0] AS $field => $data) + $fields[$field] = $data; + + unset($fields["url"]); + unset($fields["updated"]); + unset($fields["hide"]); + + // Bugfix: We had an error in the storing of keywords which lead to the "0" + // This value is still transmitted via poco. + if ($contact["keywords"] == "0") + unset($contact["keywords"]); + + if ($r[0]["keywords"] == "0") + $r[0]["keywords"] = ""; + + // assign all unassigned fields from the database entry + foreach ($fields AS $field => $data) + if (!isset($contact[$field]) OR ($contact[$field] == "")) + $contact[$field] = $r[0][$field]; + + if (!isset($contact["hide"])) + $contact["hide"] = $r[0]["hide"]; + + $fields["hide"] = $r[0]["hide"]; + + if ($contact["network"] == NETWORK_STATUSNET) + $contact["network"] = NETWORK_OSTATUS; + + // Replace alternate OStatus user format with the primary one + fix_alternate_contact_address($contact); + + if (!isset($contact["updated"])) + $contact["updated"] = datetime_convert(); + + if ($contact["server_url"] == "") { + $server_url = $contact["url"]; + + $server_url = matching_url($server_url, $contact["alias"]); + if ($server_url != "") + $contact["server_url"] = $server_url; + + $server_url = matching_url($server_url, $contact["photo"]); + if ($server_url != "") + $contact["server_url"] = $server_url; + + $server_url = matching_url($server_url, $contact["notify"]); + if ($server_url != "") + $contact["server_url"] = $server_url; + } else + $contact["server_url"] = normalise_link($contact["server_url"]); + + if (($contact["addr"] == "") AND ($contact["server_url"] != "") AND ($contact["nick"] != "")) { + $hostname = str_replace("http://", "", $contact["server_url"]); + $contact["addr"] = $contact["nick"]."@".$hostname; + } + + // Check if any field changed + $update = false; + unset($fields["generation"]); + + if ((($contact["generation"] > 0) AND ($contact["generation"] <= $r[0]["generation"])) OR ($r[0]["generation"] == 0)) { + foreach ($fields AS $field => $data) + if ($contact[$field] != $r[0][$field]) { + logger("Difference for contact ".$contact["url"]." in field '".$field."'. New value: '".$contact[$field]."', old value '".$r[0][$field]."'", LOGGER_DEBUG); + $update = true; + } + + if ($contact["generation"] < $r[0]["generation"]) { + logger("Difference for contact ".$contact["url"]." in field 'generation'. new value: '".$contact["generation"]."', old value '".$r[0]["generation"]."'", LOGGER_DEBUG); + $update = true; + } + } + + if ($update) { + logger("Update gcontact for ".$contact["url"], LOGGER_DEBUG); + + q("UPDATE `gcontact` SET `photo` = '%s', `name` = '%s', `nick` = '%s', `addr` = '%s', `network` = '%s', + `birthday` = '%s', `gender` = '%s', `keywords` = '%s', `hide` = %d, `nsfw` = %d, + `contact-type` = %d, `alias` = '%s', `notify` = '%s', `url` = '%s', + `location` = '%s', `about` = '%s', `generation` = %d, `updated` = '%s', + `server_url` = '%s', `connect` = '%s' + WHERE `nurl` = '%s' AND (`generation` = 0 OR `generation` >= %d)", + dbesc($contact["photo"]), dbesc($contact["name"]), dbesc($contact["nick"]), + dbesc($contact["addr"]), dbesc($contact["network"]), dbesc($contact["birthday"]), + dbesc($contact["gender"]), dbesc($contact["keywords"]), intval($contact["hide"]), + intval($contact["nsfw"]), intval($contact["contact-type"]), dbesc($contact["alias"]), + dbesc($contact["notify"]), dbesc($contact["url"]), dbesc($contact["location"]), + dbesc($contact["about"]), intval($contact["generation"]), dbesc($contact["updated"]), + dbesc($contact["server_url"]), dbesc($contact["connect"]), + dbesc(normalise_link($contact["url"])), intval($contact["generation"])); + + + // Now update the contact entry with the user id "0" as well. + // This is used for the shadow copies of public items. + $r = q("SELECT `id` FROM `contact` WHERE `nurl` = '%s' AND `uid` = 0 ORDER BY `id` LIMIT 1", + dbesc(normalise_link($contact["url"]))); + + if ($r) { + logger("Update shadow contact ".$r[0]["id"], LOGGER_DEBUG); + + update_contact_avatar($contact["photo"], 0, $r[0]["id"]); + + q("UPDATE `contact` SET `name` = '%s', `nick` = '%s', `addr` = '%s', + `network` = '%s', `bd` = '%s', `gender` = '%s', + `keywords` = '%s', `alias` = '%s', `contact-type` = %d, + `url` = '%s', `location` = '%s', `about` = '%s' + WHERE `id` = %d", + dbesc($contact["name"]), dbesc($contact["nick"]), dbesc($contact["addr"]), + dbesc($contact["network"]), dbesc($contact["birthday"]), dbesc($contact["gender"]), + dbesc($contact["keywords"]), dbesc($contact["alias"]), intval($contact["contact-type"]), + dbesc($contact["url"]), dbesc($contact["location"]), dbesc($contact["about"]), + intval($r[0]["id"])); + } + } + + return $gcontact_id; +} + +/** + * @brief Updates the gcontact entry from probe + * + * @param str $url profile link + */ +function update_gcontact_from_probe($url) { + $data = probe_url($url); + + if (in_array($data["network"], array(NETWORK_PHANTOM))) { + logger("Invalid network for contact url ".$data["url"]." - Called by: ".App::callstack(), LOGGER_DEBUG); + return; + } + + update_gcontact($data); +} + +/** + * @brief Update the gcontact entry for a given user id + * + * @param int $uid User ID + */ +function update_gcontact_for_user($uid) { + $r = q("SELECT `profile`.`locality`, `profile`.`region`, `profile`.`country-name`, + `profile`.`name`, `profile`.`about`, `profile`.`gender`, + `profile`.`pub_keywords`, `profile`.`dob`, `profile`.`photo`, + `profile`.`net-publish`, `user`.`nickname`, `user`.`hidewall`, + `contact`.`notify`, `contact`.`url`, `contact`.`addr` + FROM `profile` + INNER JOIN `user` ON `user`.`uid` = `profile`.`uid` + INNER JOIN `contact` ON `contact`.`uid` = `profile`.`uid` + WHERE `profile`.`uid` = %d AND `profile`.`is-default` AND `contact`.`self`", + intval($uid)); + + $location = formatted_location(array("locality" => $r[0]["locality"], "region" => $r[0]["region"], + "country-name" => $r[0]["country-name"])); + + // The "addr" field was added in 3.4.3 so it can be empty for older users + if ($r[0]["addr"] != "") + $addr = $r[0]["nickname"].'@'.str_replace(array("http://", "https://"), "", App::get_baseurl()); + else + $addr = $r[0]["addr"]; + + $gcontact = array("name" => $r[0]["name"], "location" => $location, "about" => $r[0]["about"], + "gender" => $r[0]["gender"], "keywords" => $r[0]["pub_keywords"], + "birthday" => $r[0]["dob"], "photo" => $r[0]["photo"], + "notify" => $r[0]["notify"], "url" => $r[0]["url"], + "hide" => ($r[0]["hidewall"] OR !$r[0]["net-publish"]), + "nick" => $r[0]["nickname"], "addr" => $addr, + "connect" => $addr, "server_url" => App::get_baseurl(), + "generation" => 1, "network" => NETWORK_DFRN); + + update_gcontact($gcontact); +} + +/** + * @brief Fetches users of given GNU Social server + * + * If the "Statistics" plugin is enabled (See http://gstools.org/ for details) we query user data with this. + * + * @param str $server Server address + */ +function gs_fetch_users($server) { + + logger("Fetching users from GNU Social server ".$server, LOGGER_DEBUG); + + $url = $server."/main/statistics"; + + $result = z_fetch_url($url); + if (!$result["success"]) + return false; + + $statistics = json_decode($result["body"]); + + if (is_object($statistics->config)) { + if ($statistics->config->instance_with_ssl) + $server = "https://"; + else + $server = "http://"; + + $server .= $statistics->config->instance_address; + + $hostname = $statistics->config->instance_address; + } else { + if ($statistics->instance_with_ssl) + $server = "https://"; + else + $server = "http://"; + + $server .= $statistics->instance_address; + + $hostname = $statistics->instance_address; + } + + if (is_object($statistics->users)) + foreach ($statistics->users AS $nick => $user) { + $profile_url = $server."/".$user->nickname; + + $contact = array("url" => $profile_url, + "name" => $user->fullname, + "addr" => $user->nickname."@".$hostname, + "nick" => $user->nickname, + "about" => $user->bio, + "network" => NETWORK_OSTATUS, + "photo" => App::get_baseurl()."/images/person-175.jpg"); + get_gcontact_id($contact); + } +} + +/** + * @brief Asking GNU Social server on a regular base for their user data + * + */ +function gs_discover() { + + $requery_days = intval(get_config("system", "poco_requery_days")); + + $last_update = date("c", time() - (60 * 60 * 24 * $requery_days)); + + $r = q("SELECT `nurl`, `url` FROM `gserver` WHERE `last_contact` >= `last_failure` AND `network` = '%s' AND `last_poco_query` < '%s' ORDER BY RAND() LIMIT 5", + dbesc(NETWORK_OSTATUS), dbesc($last_update)); + + if (!$r) + return; + + foreach ($r AS $server) { + gs_fetch_users($server["url"]); + q("UPDATE `gserver` SET `last_poco_query` = '%s' WHERE `nurl` = '%s'", dbesc(datetime_convert()), dbesc($server["nurl"])); + } +} ?> diff --git a/include/spool_post.php b/include/spool_post.php new file mode 100644 index 0000000000..b4cce46b57 --- /dev/null +++ b/include/spool_post.php @@ -0,0 +1,77 @@ + diff --git a/include/tags.php b/include/tags.php index a8bcae86dc..0a09438478 100644 --- a/include/tags.php +++ b/include/tags.php @@ -1,13 +1,11 @@ get_baseurl(); + $profile_base = App::get_baseurl(); $profile_data = parse_url($profile_base); $profile_base_friendica = $profile_data['host'].$profile_data['path']."/profile/"; $profile_base_diaspora = $profile_data['host'].$profile_data['path']."/u/"; - $searchpath = $a->get_baseurl()."/search?tag="; + $searchpath = App::get_baseurl()."/search?tag="; $messages = q("SELECT `guid`, `uid`, `id`, `edited`, `deleted`, `created`, `received`, `title`, `body`, `tag`, `parent` FROM `item` WHERE `id` = %d LIMIT 1", intval($itemid)); diff --git a/include/tagupdate.php b/include/tagupdate.php index b12e809772..b4de121e9f 100644 --- a/include/tagupdate.php +++ b/include/tagupdate.php @@ -1,4 +1,7 @@ replace) * @return string substituted string */ @@ -20,10 +21,10 @@ function replace_macros($s,$r) { $stamp1 = microtime(true); $a = get_app(); - + // pass $baseurl to all templates - $r['$baseurl'] = z_root(); - + $r['$baseurl'] = App::get_baseurl(); + $t = $a->template_engine(); try { @@ -275,7 +276,7 @@ if(! function_exists('paginate_data')) { * @param int $count [optional] item count (used with alt pager) * @return Array data for pagination template */ -function paginate_data(&$a, $count=null) { +function paginate_data(App $a, $count=null) { $stripped = preg_replace('/([&?]page=[0-9]*)/','',$a->query_string); $stripped = str_replace('q=','',$stripped); @@ -285,7 +286,7 @@ function paginate_data(&$a, $count=null) { if (($a->page_offset != "") AND !preg_match('/[?&].offset=/', $stripped)) $stripped .= "&offset=".urlencode($a->page_offset); - $url = z_root() . '/' . $stripped; + $url = $stripped; $data = array(); function _l(&$d, $name, $url, $text, $class="") { @@ -368,7 +369,7 @@ if(! function_exists('paginate')) { * @param App $a App instance * @return string html for pagination #FIXME remove html */ -function paginate(&$a) { +function paginate(App $a) { $data = paginate_data($a); $tpl = get_markup_template("paginate.tpl"); @@ -383,7 +384,7 @@ if(! function_exists('alt_pager')) { * @param int $i * @return string html for pagination #FIXME remove html */ -function alt_pager(&$a, $i) { +function alt_pager(App $a, $i) { $data = paginate_data($a, $i); $tpl = get_markup_template("paginate.tpl"); @@ -490,7 +491,7 @@ function item_new_uri($hostname,$uid, $guid = "") { $r = q("SELECT `id` FROM `item` WHERE `uri` = '%s' LIMIT 1", dbesc($uri)); - if(count($r)) + if (dbm::is_result($r)) $dups = true; } while($dups == true); return $uri; @@ -514,7 +515,7 @@ function photo_new_resource() { $r = q("SELECT `id` FROM `photo` WHERE `resource-id` = '%s' LIMIT 1", dbesc($resource) ); - if(count($r)) + if (dbm::is_result($r)) $found = true; } while($found == true); return $resource; @@ -580,14 +581,14 @@ function get_intltext_template($s) { if(! isset($lang)) $lang = 'en'; - if(file_exists("view/$lang$engine/$s")) { + if(file_exists("view/lang/$lang$engine/$s")) { $stamp1 = microtime(true); - $content = file_get_contents("view/$lang$engine/$s"); + $content = file_get_contents("view/lang/$lang$engine/$s"); $a->save_timestamp($stamp1, "file"); return $content; - } elseif(file_exists("view/en$engine/$s")) { + } elseif(file_exists("view/lang/en$engine/$s")) { $stamp1 = microtime(true); - $content = file_get_contents("view/en$engine/$s"); + $content = file_get_contents("view/lang/en$engine/$s"); $a->save_timestamp($stamp1, "file"); return $content; } else { @@ -677,11 +678,13 @@ function attribute_contains($attr,$s) { return false; }} -if(! function_exists('logger')) { +if (! function_exists('logger')) { /* setup int->string log level map */ $LOGGER_LEVELS = array(); /** + * @brief Logs the given message at the given log level + * * log levels: * LOGGER_NORMAL (default) * LOGGER_TRACE @@ -691,46 +694,63 @@ $LOGGER_LEVELS = array(); * * @global App $a * @global dba $db + * @global array $LOGGER_LEVELS * @param string $msg * @param int $level */ -function logger($msg,$level = 0) { - // turn off logger in install mode - global $a; +function logger($msg, $level = 0) { + $a = get_app(); global $db; global $LOGGER_LEVELS; - if(($a->module == 'install') || (! ($db && $db->connected))) return; - - if (count($LOGGER_LEVELS)==0){ - foreach (get_defined_constants() as $k=>$v){ - if (substr($k,0,7)=="LOGGER_") - $LOGGER_LEVELS[$v] = substr($k,7,7); - } + // turn off logger in install mode + if ( + $a->module == 'install' + || ! ($db && $db->connected) + ) { + return; } $debugging = get_config('system','debugging'); - $loglevel = intval(get_config('system','loglevel')); $logfile = get_config('system','logfile'); + $loglevel = intval(get_config('system','loglevel')); - if((! $debugging) || (! $logfile) || ($level > $loglevel)) + if ( + ! $debugging + || ! $logfile + || $level > $loglevel + ) { return; + } + + if (count($LOGGER_LEVELS) == 0) { + foreach (get_defined_constants() as $k => $v) { + if (substr($k, 0, 7) == "LOGGER_") { + $LOGGER_LEVELS[$v] = substr($k, 7, 7); + } + } + } + + $process_id = session_id(); + + if ($process_id == '') { + $process_id = get_app()->process_id; + } $callers = debug_backtrace(); - $logline = sprintf("%s@%s\t[%s]:%s:%s:%s\t%s\n", - datetime_convert(), - session_id(), - $LOGGER_LEVELS[$level], - basename($callers[0]['file']), - $callers[0]['line'], - $callers[1]['function'], - $msg - ); + $logline = sprintf("%s@%s\t[%s]:%s:%s:%s\t%s\n", + datetime_convert(), + $process_id, + $LOGGER_LEVELS[$level], + basename($callers[0]['file']), + $callers[0]['line'], + $callers[1]['function'], + $msg + ); $stamp1 = microtime(true); @file_put_contents($logfile, $logline, FILE_APPEND); $a->save_timestamp($stamp1, "file"); - return; }} @@ -749,71 +769,75 @@ function activity_match($haystack,$needle) { }} -if(! function_exists('get_tags')) { /** - * Pull out all #hashtags and @person tags from $s; + * @brief Pull out all #hashtags and @person tags from $string. + * * We also get @person@domain.com - which would make * the regex quite complicated as tags can also * end a sentence. So we'll run through our results * and strip the period from any tags which end with one. * Returns array of tags found, or empty array. * - * @param string $s - * @return array + * @param string $string Post content + * @return array List of tag and person names */ -function get_tags($s) { +function get_tags($string) { $ret = array(); // Convert hashtag links to hashtags - $s = preg_replace("/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism", "#$2", $s); + $string = preg_replace('/#\[url\=([^\[\]]*)\](.*?)\[\/url\]/ism', '#$2', $string); // ignore anything in a code block - $s = preg_replace('/\[code\](.*?)\[\/code\]/sm','',$s); + $string = preg_replace('/\[code\](.*?)\[\/code\]/sm', '', $string); // Force line feeds at bbtags - $s = str_replace(array("[", "]"), array("\n[", "]\n"), $s); + $string = str_replace(array('[', ']'), array("\n[", "]\n"), $string); // ignore anything in a bbtag - $s = preg_replace('/\[(.*?)\]/sm','',$s); + $string = preg_replace('/\[(.*?)\]/sm', '', $string); // Match full names against @tags including the space between first and last // We will look these up afterward to see if they are full names or not recognisable. - if(preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/',$s,$match)) { - foreach($match[1] as $mtch) { - if(strstr($mtch,"]")) { + if (preg_match_all('/(@[^ \x0D\x0A,:?]+ [^ \x0D\x0A@,:?]+)([ \x0D\x0A@,:?]|$)/', $string, $matches)) { + foreach ($matches[1] as $match) { + if (strstr($match, ']')) { // we might be inside a bbcode color tag - leave it alone continue; } - if(substr($mtch,-1,1) === '.') - $ret[] = substr($mtch,0,-1); - else - $ret[] = $mtch; + if (substr($match, -1, 1) === '.') { + $ret[] = substr($match, 0, -1); + } else { + $ret[] = $match; + } } } // Otherwise pull out single word tags. These can be @nickname, @first_last // and #hash tags. - if(preg_match_all('/([!#@][^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/',$s,$match)) { - foreach($match[1] as $mtch) { - if(strstr($mtch,"]")) { + if (preg_match_all('/([!#@][^\^ \x0D\x0A,;:?]+)([ \x0D\x0A,;:?]|$)/', $string, $matches)) { + foreach($matches[1] as $match) { + if (strstr($match, ']')) { // we might be inside a bbcode color tag - leave it alone continue; } - if(substr($mtch,-1,1) === '.') - $mtch = substr($mtch,0,-1); + if (substr($match, -1, 1) === '.') { + $match = substr($match,0,-1); + } // ignore strictly numeric tags like #1 - if((strpos($mtch,'#') === 0) && ctype_digit(substr($mtch,1))) + if ((strpos($match, '#') === 0) && ctype_digit(substr($match, 1))) { continue; + } // try not to catch url fragments - if(strpos($s,$mtch) && preg_match('/[a-zA-z0-9\/]/',substr($s,strpos($s,$mtch)-1,1))) + if (strpos($string, $match) && preg_match('/[a-zA-z0-9\/]/', substr($string, strpos($string, $match) - 1, 1))) { continue; - $ret[] = $mtch; + } + $ret[] = $match; } } return $ret; -}} +} // @@ -829,35 +853,6 @@ function qp($s) { return str_replace ("%","=",rawurlencode($s)); }} - - -if(! function_exists('get_mentions')) { -/** - * @param array $item - * @return string html for mentions #FIXME: remove html - */ -function get_mentions($item) { - $o = ''; - if(! strlen($item['tag'])) - return $o; - - $arr = explode(',',$item['tag']); - foreach($arr as $x) { - $matches = null; - if(preg_match('/@\[url=([^\]]*)\]/',$x,$matches)) { - $o .= "\t\t" . '' . "\r\n"; - $o .= "\t\t" . '' . "\r\n"; - } - } - - if (!$item['private']) { - $o .= "\t\t".''."\r\n"; - $o .= "\t\t".''."\r\n"; - } - - return $o; -}} - if(! function_exists('contact_block')) { /** * Get html for contact block. @@ -879,15 +874,15 @@ function contact_block() { if((! is_array($a->profile)) || ($a->profile['hide-friends'])) return $o; $r = q("SELECT COUNT(*) AS `total` FROM `contact` - WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 and `pending` = 0 - AND `hidden` = 0 AND `archive` = 0 + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` + AND NOT `pending` AND NOT `hidden` AND NOT `archive` AND `network` IN ('%s', '%s', '%s')", intval($a->profile['uid']), dbesc(NETWORK_DFRN), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DIASPORA) ); - if(count($r)) { + if (dbm::is_result($r)) { $total = intval($r[0]['total']); } if(! $total) { @@ -895,21 +890,32 @@ function contact_block() { $micropro = Null; } else { - $r = q("SELECT * FROM `contact` - WHERE `uid` = %d AND `self` = 0 AND `blocked` = 0 and `pending` = 0 - AND `hidden` = 0 AND `archive` = 0 - AND `network` IN ('%s', '%s', '%s') ORDER BY RAND() LIMIT %d", + // Splitting the query in two parts makes it much faster + $r = q("SELECT `id` FROM `contact` + WHERE `uid` = %d AND NOT `self` AND NOT `blocked` + AND NOT `pending` AND NOT `hidden` AND NOT `archive` + AND `network` IN ('%s', '%s', '%s') + ORDER BY RAND() LIMIT %d", intval($a->profile['uid']), dbesc(NETWORK_DFRN), dbesc(NETWORK_OSTATUS), dbesc(NETWORK_DIASPORA), intval($shown) ); - if(count($r)) { - $contacts = sprintf( tt('%d Contact','%d Contacts', $total),$total); - $micropro = Array(); - foreach($r as $rr) { - $micropro[] = micropro($rr,true,'mpfriend'); + if (dbm::is_result($r)) { + $contacts = ""; + foreach ($r AS $contact) + $contacts[] = $contact["id"]; + + $r = q("SELECT `id`, `uid`, `addr`, `url`, `name`, `thumb`, `network` FROM `contact` WHERE `id` IN (%s)", + dbesc(implode(",", $contacts))); + + if (dbm::is_result($r)) { + $contacts = sprintf( tt('%d Contact','%d Contacts', $total),$total); + $micropro = Array(); + foreach ($r as $rr) { + $micropro[] = micropro($rr,true,'mpfriend'); + } } } } @@ -929,20 +935,28 @@ function contact_block() { }} -if(! function_exists('micropro')) { /** + * @brief Format contacts as picture links or as texxt links * - * @param array $contact - * @param boolean $redirect - * @param string $class - * @param boolean $textmode - * @return string #FIXME: remove html + * @param array $contact Array with contacts which contains an array with + * int 'id' => The ID of the contact + * int 'uid' => The user ID of the user who owns this data + * string 'name' => The name of the contact + * string 'url' => The url to the profile page of the contact + * string 'addr' => The webbie of the contact (e.g.) username@friendica.com + * string 'network' => The network to which the contact belongs to + * string 'thumb' => The contact picture + * string 'click' => js code which is performed when clicking on the contact + * @param boolean $redirect If true try to use the redir url if it's possible + * @param string $class CSS class for the + * @param boolean $textmode If true display the contacts as text links + * if false display the contacts as picture links + + * @return string Formatted html */ function micropro($contact, $redirect = false, $class = '', $textmode = false) { - if($class) - $class = ' ' . $class; - + // Use the contact URL if no address is available if ($contact["addr"] == "") $contact["addr"] = $contact["url"]; @@ -952,7 +966,7 @@ function micropro($contact, $redirect = false, $class = '', $textmode = false) { if($redirect) { $a = get_app(); - $redirect_url = z_root() . '/redir/' . $contact['id']; + $redirect_url = 'redir/' . $contact['id']; if(local_user() && ($contact['uid'] == local_user()) && ($contact['network'] === NETWORK_DFRN)) { $redir = true; $url = $redirect_url; @@ -961,26 +975,23 @@ function micropro($contact, $redirect = false, $class = '', $textmode = false) { else $url = zrl($url); } - $click = ((x($contact,'click')) ? ' onclick="' . $contact['click'] . '" ' : ''); - if($click) + + // If there is some js available we don't need the url + if(x($contact,'click')) $url = ''; - if($textmode) { - return '' . "\r\n"; - } - else { - return '
' . $contact['name']
-			. '
' . "\r\n"; - } -}} + + return replace_macros(get_markup_template(($textmode)?'micropro_txt.tpl':'micropro_img.tpl'),array( + '$click' => (($contact['click']) ? $contact['click'] : ''), + '$class' => $class, + '$url' => $url, + '$photo' => proxy_url($contact['thumb'], false, PROXY_SIZE_THUMB), + '$name' => $contact['name'], + 'title' => $contact['name'] . ' [' . $contact['addr'] . ']', + '$parkle' => $sparkle, + '$redir' => $redir, + + )); +} @@ -993,16 +1004,17 @@ if(! function_exists('search')) { * @param string $url search url * @param boolean $savedsearch show save search button */ -function search($s,$id='search-box',$url='/search',$save = false, $aside = true) { +function search($s,$id='search-box',$url='search',$save = false, $aside = true) { $a = get_app(); $values = array( - '$s' => $s, + '$s' => htmlspecialchars($s), '$id' => $id, - '$action_url' => $a->get_baseurl((stristr($url,'network')) ? true : false) . $url, + '$action_url' => $url, '$search_label' => t('Search'), '$save_label' => t('Save'), '$savedsearch' => feature_enabled(local_user(),'savedsearch'), + '$search_hint' => t('@name, !forum, #tags, content'), ); if (!$aside) { @@ -1108,160 +1120,6 @@ function get_mood_verbs() { return $arr; } - - -if(! function_exists('smilies')) { -/** - * Replaces text emoticons with graphical images - * - * It is expected that this function will be called using HTML text. - * We will escape text between HTML pre and code blocks from being - * processed. - * - * At a higher level, the bbcode [nosmile] tag can be used to prevent this - * function from being executed by the prepare_text() routine when preparing - * bbcode source for HTML display - * - * @param string $s - * @param boolean $sample - * @return string - * @hook smilie ('texts' => smilies texts array, 'icons' => smilies html array, 'string' => $s) - */ -function smilies($s, $sample = false) { - $a = get_app(); - - if(intval(get_config('system','no_smilies')) - || (local_user() && intval(get_pconfig(local_user(),'system','no_smilies')))) - return $s; - - $s = preg_replace_callback('/
(.*?)<\/pre>/ism','smile_encode',$s);
-	$s = preg_replace_callback('/(.*?)<\/code>/ism','smile_encode',$s);
-
-	$texts =  array(
-		'<3',
-		'</3',
-		'<\\3',
-		':-)',
-		';-)',
-		':-(',
-		':-P',
-		':-p',
-		':-"',
-		':-"',
-		':-x',
-		':-X',
-		':-D',
-		'8-|',
-		'8-O',
-		':-O',
-		'\\o/',
-		'o.O',
-		'O.o',
-		'o_O',
-		'O_o',
-		":'(",
-		":-!",
-		":-/",
-		":-[",
-		"8-)",
-		':beer',
-		':homebrew',
-		':coffee',
-		':facepalm',
-		':like',
-		':dislike',
-		'~friendica',
-		'red#',
-		'red#matrix'
-
-	);
-
-	$icons = array(
-		'<3',
-		'</3',
-		'<\\3',
-		':-)',
-		';-)',
-		':-(',
-		':-P',
-		':-p',
-		':-\',
-		':-\',
-		':-x',
-		':-X',
-		':-D',
-		'8-|',
-		'8-O',
-		':-O',
-		'\\o/',
-		'o.O',
-		'O.o',
-		'o_O',
-		'O_o',
-		':\'(',
-		':-!',
-		':-/',
-		':-[',
-		'8-)',
-		':beer',
-		':homebrew',
-		':coffee',
-		':facepalm',
-		':like',
-		':dislike',
-		'~friendica ~friendica',
-		'redredmatrix',
-		'redredmatrix'
-	);
-
-	$params = array('texts' => $texts, 'icons' => $icons, 'string' => $s);
-	call_hooks('smilie', $params);
-
-	if($sample) {
-		$s = '
'; - for($x = 0; $x < count($params['texts']); $x ++) { - $s .= '
' . $params['texts'][$x] . '
' . $params['icons'][$x] . '
'; - } - } - else { - $params['string'] = preg_replace_callback('/<(3+)/','preg_heart',$params['string']); - $s = str_replace($params['texts'],$params['icons'],$params['string']); - } - - $s = preg_replace_callback('/
(.*?)<\/pre>/ism','smile_decode',$s);
-	$s = preg_replace_callback('/(.*?)<\/code>/ism','smile_decode',$s);
-
-	return $s;
-
-}}
-
-function smile_encode($m) {
-	return(str_replace($m[1],base64url_encode($m[1]),$m[0]));
-}
-
-function smile_decode($m) {
-	return(str_replace($m[1],base64url_decode($m[1]),$m[0]));
-}
-
-
-/**
- * expand <3333 to the correct number of hearts
- *
- * @param string $x
- * @return string
- */
-function preg_heart($x) {
-	$a = get_app();
-	if(strlen($x[1]) == 1)
-		return $x[0];
-	$t = '';
-	for($cnt = 0; $cnt < strlen($x[1]); $cnt ++)
-		$t .= '<3';
-	$r =  str_replace($x[0],$t,$x[0]);
-	return $r;
-}
-
-
 if(! function_exists('day_translate')) {
 /**
  * Translate days and months names
@@ -1314,33 +1172,29 @@ function link_compare($a,$b) {
 	return false;
 }}
 
-
-if(! function_exists('redir_private_images')) {
 /**
- * Find any non-embedded images in private items and add redir links to them
+ * @brief Find any non-embedded images in private items and add redir links to them
  *
  * @param App $a
- * @param array $item
+ * @param array &$item The field array of an item row
  */
-function redir_private_images($a, &$item) {
-
+function redir_private_images($a, &$item)
+{
 	$matches = false;
 	$cnt = preg_match_all('|\[img\](http[^\[]*?/photo/[a-fA-F0-9]+?(-[0-9]\.[\w]+?)?)\[\/img\]|', $item['body'], $matches, PREG_SET_ORDER);
-	if($cnt) {
-		//logger("redir_private_images: matches = " . print_r($matches, true));
-		foreach($matches as $mtch) {
-			if(strpos($mtch[1], '/redir') !== false)
+	if ($cnt) {
+		foreach ($matches as $mtch) {
+			if (strpos($mtch[1], '/redir') !== false) {
 				continue;
+			}
 
-			if((local_user() == $item['uid']) && ($item['private'] != 0) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
-				//logger("redir_private_images: redir");
-				$img_url = z_root() . '/redir?f=1&quiet=1&url=' . $mtch[1] . '&conurl=' . $item['author-link'];
-				$item['body'] = str_replace($mtch[0], "[img]".$img_url."[/img]", $item['body']);
+			if ((local_user() == $item['uid']) && ($item['private'] != 0) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN)) {
+				$img_url = 'redir?f=1&quiet=1&url=' . urlencode($mtch[1]) . '&conurl=' . urlencode($item['author-link']);
+				$item['body'] = str_replace($mtch[0], '[img]' . $img_url . '[/img]', $item['body']);
 			}
 		}
 	}
-
-}}
+}
 
 function put_item_in_cache(&$item, $update = false) {
 
@@ -1415,7 +1269,14 @@ function prepare_body(&$item,$attach = false, $preview = false) {
 	$item['hashtags'] = $hashtags;
 	$item['mentions'] = $mentions;
 
-	put_item_in_cache($item, true);
+	// Update the cached values if there is no "zrl=..." on the links
+	$update = (!local_user() and !remote_user() and ($item["uid"] == 0));
+
+	// Or update it if the current viewer is the intented viewer
+	if (($item["uid"] == local_user()) AND ($item["uid"] != 0))
+		$update = true;
+
+	put_item_in_cache($item, $update);
 	$s = $item["rendered-html"];
 
 	$prep_arr = array('item' => $item, 'html' => $s, 'preview' => $preview);
@@ -1443,7 +1304,7 @@ function prepare_body(&$item,$attach = false, $preview = false) {
 					$mime = $mtch[3];
 
 					if((local_user() == $item['uid']) && ($item['contact-id'] != $a->contact['id']) && ($item['network'] == NETWORK_DFRN))
-						$the_url = z_root() . '/redir/' . $item['contact-id'] . '?f=1&url=' . $mtch[1];
+						$the_url = 'redir/' . $item['contact-id'] . '?f=1&url=' . $mtch[1];
 					else
 						$the_url = $mtch[1];
 
@@ -1507,7 +1368,7 @@ function prepare_body(&$item,$attach = false, $preview = false) {
 	// map
 	if(strpos($s,'
') !== false && $item['coord']) { $x = generate_map(trim($item['coord'])); - if($x) { + if ($x) { $s = preg_replace('/\
/','$0' . $x,$s); } } @@ -1526,7 +1387,7 @@ function prepare_body(&$item,$attach = false, $preview = false) { $pos = strpos($s, $spoilersearch); $rnd = random_string(8); - $spoilerreplace = '
'.sprintf(t('Click to open/close')).''. + $spoilerreplace = '
'.sprintf(t('Click to open/close')).''. '