<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * The information is presented at the "api/nodeinfo/2.0.json" endpoint.
 *
 * @package   NodeInfo
 * @author    Stéphane Bérubé <chimo@chromic.org>
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * NodeInfo 2.0
 *
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Nodeinfo_2_0Action extends Action
{
    private $plugins;

    protected function handle(): void
    {
        parent::handle();
        header('Access-Control-Allow-Origin: *');
        $this->plugins = $this->getActivePluginList();
        $this->showNodeInfo();
    }

    /**
     * Most functionality depends on the active plugins, this gives us enough information concerning that
     *
     * @return array
     * @author Stéphane Bérubé <chimo@chromic.org>
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function getActivePluginList(): array
    {
        $plugin_version = [];
        $plugins = [];

        Event::handle('PluginVersion', [&$plugin_version]);

        foreach ($plugin_version as $plugin) {
            $plugins[str_replace(' ', '', strtolower($plugin['name']))] = true;
        }

        return $plugins;
    }

    /**
     * The NodeInfo page
     *
     * @return void
     * @author Stéphane Bérubé <chimo@chromic.org>
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function showNodeInfo(): void
    {
        $openRegistrations = $this->getRegistrationsStatus();
        $userCount = $this->getUserCount();
        $postCount = $this->getPostCount();
        $commentCount = $this->getCommentCount();

        $usersActiveHalfyear = $this->getActiveUsers(180);
        $usersActiveMonth = $this->getActiveUsers(30);

        $protocols = $this->getProtocols();
        $inboundServices = $this->getInboundServices();
        $outboundServices = $this->getOutboundServices();

        $metadata = $this->getMetadata();

        /* Required NodeInfo fields
              "version",
              "software",
              "protocols",
              "services",
              "openRegistrations",
              "usage",
              "metadata"
         */

        $json = json_encode([
            // The schema version, must be 2.0.
            'version' => '2.0',

            // [Mandatory] Metadata about server software in use.
            'software' => [
                'name' => 'gnusocial', // The canonical name of this server software.
                'version' => GNUSOCIAL_VERSION // The version of this server software.
            ],

            // The protocols supported on this server.
            // The spec requires an array containing at least 1 item but we can't ensure that.
            'protocols' => $protocols,

            // The third party sites this server can connect to via their application API.
            'services' => [
                // The third party sites this server can retrieve messages from for combined display with regular traffic.
                'inbound' => $inboundServices,
                // The third party sites this server can publish messages to on the behalf of a user.
                'outbound' => $outboundServices
            ],

            // Whether this server allows open self-registration.
            'openRegistrations' => $openRegistrations,

            // Usage statistics for this server.
            'usage' => [
                'users' => [
                    // The total amount of on this server registered users.
                    'total' => $userCount,
                    // The amount of users that signed in at least once in the last 180 days.
                    'activeHalfyear' => $usersActiveHalfyear,
                    // The amount of users that signed in at least once in the last 30 days.
                    'activeMonth' => $usersActiveMonth
                ],
                // The amount of posts that were made by users that are registered on this server.
                'localPosts' => $postCount,
                // The amount of comments that were made by users that are registered on this server.
                'localComments' => $commentCount
            ],

            // Free form key value pairs for software specific values. Clients should not rely on any specific key present.
            'metadata' => $metadata
        ]);

        header('Content-Type: application/json; profile=http://nodeinfo.diaspora.software/ns/schema/2.0#; charset=utf-8');
        print $json;
    }

    /**
     * The protocols supported on this server.
     * The spec requires an array containing at least 1 item but we can't ensure that
     *
     * These can only be one of:
     *  - activitypub,
     *  - buddycloud,
     *  - dfrn,
     *  - diaspora,
     *  - libertree,
     *  - ostatus,
     *  - pumpio,
     *  - tent,
     *  - xmpp,
     *  - zot
     *
     * @return array
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function getProtocols(): array
    {
        $protocols = [];

        Event::handle('NodeInfoProtocols', [&$protocols]);

        return $protocols;
    }

    /**
     * The third party sites this server can retrieve messages from for combined display with regular traffic.
     *
     * These can only be one of:
     *  - atom1.0,
     *  - gnusocial,
     *  - imap,
     *  - pnut,
     *  - pop3,
     *  - pumpio,
     *  - rss2.0,
     *  - twitter
     *
     * @return array
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getInboundServices(): array
    {
        $inboundServices = [];
        $ostatusEnabled = array_key_exists('ostatus', $this->plugins);

        // We need those two to read feeds (despite WebSub).
        if ($ostatusEnabled && array_key_exists('feedpoller', $this->plugins)) {
            $inboundServices[] = 'atom1.0';
            $inboundServices[] = 'rss2.0';
        }

        if (array_key_exists('twitterbridge', $this->plugins) && common_config('twitterimport', 'enabled')) {
            $inboundServices[] = 'twitter';
        }

        if (array_key_exists('imap', $this->plugins)) {
            $inboundServices[] = 'imap';
        }

        // We can receive messages from another GNU social instance if we have at least one of those enabled.
        // And the same happens in the other instance
        if ($ostatusEnabled || array_key_exists('activitypub', $this->plugins)) {
            $inboundServices[] = 'gnusocial';
        }

        return $inboundServices;
    }

    /**
     * The third party sites this server can publish messages to on the behalf of a user.
     *
     * These can only be one of:
     *  - atom1.0,
     *  - blogger,
     *  - buddycloud,
     *  - diaspora,
     *  - dreamwidth,
     *  - drupal,
     *  - facebook,
     *  - friendica,
     *  - gnusocial,
     *  - google,
     *  - insanejournal,
     *  - libertree,
     *  - linkedin,
     *  - livejournal,
     *  - mediagoblin,
     *  - myspace,
     *  - pinterest,
     *  - pnut,
     *  - posterous,
     *  - pumpio,
     *  - redmatrix,
     *  - rss2.0,
     *  - smtp,
     *  - tent,
     *  - tumblr,
     *  - twitter,
     *  - wordpress,
     *  - xmpp
     *
     * @return array
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getOutboundServices(): array
    {
        // Those two are always available
        $outboundServices = ['atom1.0', 'rss2.0'];

        if (array_key_exists('twitterbridge', $this->plugins)) {
            $outboundServices[] = 'twitter';
        }

        // We can send messages to another GNU social instance if we have at least one of those enabled.
        // And the same happens in the other instance
        if (array_key_exists('ostatus', $this->plugins) ||
            array_key_exists('activitypub', $this->plugins)) {
            $outboundServices[] = 'gnusocial';
        }

        $xmppEnabled = (array_key_exists('xmpp', $this->plugins) && common_config('xmpp', 'enabled')) ? true : false;
        if ($xmppEnabled) {
            $outboundServices[] = 'xmpp';
        }

        return $outboundServices;
    }

    /**
     * Whether this server allows open self-registration.
     *
     * @return bool
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getRegistrationsStatus(): bool
    {
        $areRegistrationsClosed = (common_config('site', 'closed')) ? true : false;
        $isSiteInviteOnly = (common_config('site', 'inviteonly')) ? true : false;

        return !($areRegistrationsClosed || $isSiteInviteOnly);
    }

    /**
     * The total amount of on this server registered users.
     *
     * @return int
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getUserCount(): int
    {
        $users = new Usage_stats();
        $userCount = $users->getUserCount();

        return $userCount;
    }

    /**
     * The amount of users that were active at least once in the last $days days.
     *
     * Technically, the NodeInfo spec defines 'active' as 'signed in at least once in the
     * last {180, 30} days depending on request', but GNU social doesn't keep track of when
     * users last logged in.
     *
     * Therefore, we use Favourites, Notices and Date of account creation to underestimate a
     * value. Underestimate because a user that only logs in to see his feed is too an active
     * user.
     *
     * @param  int $days
     * @return int
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function getActiveUsers(int $days): int
    {
        $userTable = common_database_tablename('user');
        $query = <<<END
            SELECT COUNT(DISTINCT profile_id) AS active_users_count
              FROM (
                SELECT profile_id FROM notice
                  WHERE notice.created >= CURRENT_DATE - INTERVAL '{$days}' DAY AND notice.is_local = 1
                UNION ALL
                SELECT user_id FROM fave INNER JOIN {$userTable} ON fave.user_id = {$userTable}.id
                  WHERE fave.created >= CURRENT_DATE - INTERVAL '{$days}' DAY
                UNION ALL
                SELECT id FROM {$userTable} WHERE {$userTable}.created >= CURRENT_DATE - INTERVAL '{$days}' DAY
              ) AS source
              LEFT JOIN profile_role USING (profile_id)
              WHERE profile_role.profile_id IS NULL OR profile_role.role <> 'silenced';
            END;

        $activeUsersCount = new DB_DataObject();
        $activeUsersCount->query($query);
        $activeUsersCount->fetch();
        return $activeUsersCount->active_users_count;
    }

    /**
     * The amount of posts that were made by users that are registered on this server.
     *
     * @return int
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getPostCount(): int
    {
        $posts = new Usage_stats();
        $postCount = $posts->getPostCount();

        return $postCount;
    }

    /**
     * The amount of comments that were made by users that are registered on this server.
     *
     * @return int
     * @author Stéphane Bérubé <chimo@chromic.org>
     */
    public function getCommentCount(): int
    {
        $comments = new Usage_stats();
        $commentCount = $comments->getCommentCount();

        return $commentCount;
    }

    /**
     * Some additional information related to this GNU social instance
     *
     * @return array
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function getMetadata(): array
    {
        $metadata = [
            'nodeName' => common_config('site', 'name'),
            'software' => [
                'homepage' => GNUSOCIAL_ENGINE_URL,
                'repository' => GNUSOCIAL_ENGINE_REPO_URL,
            ],
            'uploadLimit' => common_get_preferred_php_upload_limit(),
            'postFormats' => [
                'text/plain',
                'text/html'
            ],
            'features' => []
        ];

        if (array_key_exists('poll', $this->plugins)) {
            $metadata['features'][] = 'polls';
        }

        return $metadata;
    }
}