code:
<?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/>.

/**
 * @category  Notices
 * @package   GNUsocial
 * @author    Shashi Gowda <connect2shashi@gmail.com>
 * @copyright 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();

class Profile_list extends Managed_DataObject
{
    public $__table = 'profile_list';                      // table name
    public $id;                              // int(4)  primary_key not_null
    public $tagger;                          // int(4)
    public $tag;                             // varchar(64)
    public $description;                     // text
    public $private;                         // bool    default_false
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP
    public $uri;                             // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $mainpage;                        // varchar(191)   not 255 because utf8mb4 takes more space
    public $tagged_count;                    // smallint
    public $subscriber_count;                // smallint

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
                'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
                'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'people tag'),
                'description' => array('type' => 'text', 'description' => 'description of the people tag'),
                'private' => array('type' => 'bool', 'default' => false, 'description' => 'is this tag private'),

                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date the tag was added'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date the tag was modified'),

                'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universal identifier'),
                'mainpage' => array('type' => 'varchar', 'length' => 191, 'description' => 'page to link to'),
                'tagged_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of people tagged with this tag by this user'),
                'subscriber_count' => array('type' => 'int', 'default' => 0, 'description' => 'number of subscribers to this tag'),
            ),
            'primary key' => array('tagger', 'tag'),
            'unique keys' => array(
                'profile_list_id_key' => array('id'),
            ),
            'foreign keys' => array(
                'profile_list_tagger_fkey' => array('profile', array('tagger' => 'id')),
            ),
            'indexes' => array(
                'profile_list_modified_idx' => array('modified'),
                'profile_list_tag_idx' => array('tag'),
                'profile_list_tagger_tag_idx' => array('tagger', 'tag'),
                'profile_list_tagged_count_idx' => array('tagged_count'),
                'profile_list_subscriber_count_idx' => array('subscriber_count'),
            ),
        );
    }

    /**
     * get the tagger of this profile_list object
     *
     * @return Profile the tagger
     */

    public function getTagger()
    {
        return Profile::getByID($this->tagger);
    }

    /**
     * return a string to identify this
     * profile_list in the user interface etc.
     *
     * @return String
     */

    public function getBestName()
    {
        return $this->tag;
    }

    /**
     * return a uri string for this profile_list
     *
     * @return String uri
     */

    public function getUri()
    {
        $uri = null;
        if (Event::handle('StartProfiletagGetUri', array($this, &$uri))) {
            if (!empty($this->uri)) {
                $uri = $this->uri;
            } else {
                $uri = common_local_url(
                    'profiletagbyid',
                    ['id' => $this->id, 'tagger_id' => $this->tagger]
                );
            }
        }
        Event::handle('EndProfiletagGetUri', array($this, &$uri));
        return $uri;
    }

    /**
     * return a url to the homepage of this item
     *
     * @return String home url
     */

    public function homeUrl()
    {
        $url = null;
        if (Event::handle('StartUserPeopletagHomeUrl', array($this, &$url))) {
            // normally stored in mainpage, but older ones may be null
            if (!empty($this->mainpage)) {
                $url = $this->mainpage;
            } else {
                $url = common_local_url(
                    'showprofiletag',
                    [
                        'nickname' => $this->getTagger()->nickname,
                        'tag'      => $this->tag,
                    ]
                );
            }
        }
        Event::handle('EndUserPeopletagHomeUrl', array($this, &$url));
        return $url;
    }

    /**
     * return an immutable url for this object
     *
     * @return String permalink
     */

    public function permalink()
    {
        $url = null;
        if (Event::handle('StartProfiletagPermalink', array($this, &$url))) {
            $url = common_local_url(
                'profiletagbyid',
                ['id' => $this->id]
            );
        }
        Event::handle('EndProfiletagPermalink', array($this, &$url));
        return $url;
    }

    /**
     * Query notices by users associated with this tag,
     * but first check the cache before hitting the DB.
     *
     * @param integer $offset   offset
     * @param integer $limit    maximum no of results
     * @param integer $since_id=null    since this id
     * @param integer $max_id=null  maximum id in result
     *
     * @return Notice the query
     */

    public function getNotices($offset, $limit, $since_id = null, $max_id = null)
    {
        // FIXME: Use something else than Profile::current() to avoid
        // possible confusion between session user and queue processing.
        $stream = new PeopletagNoticeStream($this, Profile::current());

        return $stream->getNotices($offset, $limit, $since_id, $max_id);
    }

    /**
     * Get subscribers (local and remote) to this people tag
     * Order by reverse chronology
     *
     * @param integer $offset   offset
     * @param integer $limit    maximum no of results
     * @param integer $since_id=null    since unix timestamp
     * @param integer $upto=null  maximum unix timestamp when subscription was made
     *
     * @return Profile results
     */

    public function getSubscribers(int $offset = 0, ?int $limit = null, int $since = 0, int $upto = 0)
    {
        $subs = new Profile();

        $subs->joinAdd(
            array('id', 'profile_tag_subscription:profile_id')
        );
        $subs->whereAdd('profile_tag_subscription.profile_tag_id = ' . $this->id);

        if (common_config('db', 'type') !== 'mysql') {
            $subs->selectAdd(sprintf(
                '((EXTRACT(DAY %1$s) * 24 + EXTRACT(HOUR %1$s)) * 60 + ' .
                'EXTRACT(MINUTE %1$s)) * 60 + FLOOR(EXTRACT(SECOND %1$s)) AS "cursor"',
                "FROM (profile_tag_subscription.created - TIMESTAMP '1970-01-01 00:00:00')"
            ));
        } else {
            $subs->selectAdd("timestampdiff(SECOND, '1970-01-01', profile_tag_subscription.created) AS `cursor`");
        }

        if ($since != 0) {
            $subs->whereAdd('cursor > ' . $since);
        }

        if ($upto != 0) {
            $subs->whereAdd('cursor <= ' . $upto);
        }

        if ($limit != null) {
            $subs->limit($offset, $limit);
        }

        $subs->orderBy('profile_tag_subscription.created DESC');
        $subs->find();

        return $subs;
    }

    /**
     * Get all and only local subscribers to this people tag
     * used for distributing notices to user inboxes.
     *
     * @return array ids of users
     */

    public function getUserSubscribers()
    {
        // XXX: cache this

        $user = new User();

        $user->query(sprintf(
            'SELECT id ' .
            'FROM %1$s INNER JOIN profile_tag_subscription ' .
            'ON %1$s.id = profile_tag_subscription.profile_id ' .
            'WHERE profile_tag_subscription.profile_tag_id = %2$d ',
            $user->escapedTableName(),
            $this->id
        ));

        $ids = [];

        while ($user->fetch()) {
            $ids[] = $user->id;
        }

        $user->free();

        return $ids;
    }

    /**
     * Check to see if a given profile has
     * subscribed to this people tag's timeline
     *
     * @param mixed $id User or Profile object or integer id
     *
     * @return boolean subscription status
     */

    public function hasSubscriber($id)
    {
        if (!is_numeric($id)) {
            $id = $id->id;
        }

        $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $this->id,
                                                       'profile_id'     => $id));
        return !empty($sub);
    }

    /**
     * Get profiles tagged with this people tag,
     * include modified timestamp as a "cursor" field
     * order by descending order of modified time
     *
     * @param integer $offset   offset
     * @param integer $limit    maximum no of results
     * @param integer $since_id=null    since unix timestamp
     * @param integer $upto=null  maximum unix timestamp when subscription was made
     *
     * @return Profile results
     */

    public function getTagged(int $offset = 0, ?int $limit = null, int $since = 0, int $upto = 0)
    {
        $tagged = new Profile();
        $tagged->joinAdd(['id', 'profile_tag:tagged']);

        if (common_config('db', 'type') !== 'mysql') {
            $tagged->selectAdd(sprintf(
                '((EXTRACT(DAY %1$s) * 24 + EXTRACT(HOUR %1$s)) * 60 + ' .
                'EXTRACT(MINUTE %1$s)) * 60 + FLOOR(EXTRACT(SECOND %1$s)) AS "cursor"',
                "FROM (profile_tag.modified - TIMESTAMP '1970-01-01 00:00:00')"
            ));
        } else {
            $tagged->selectAdd("timestampdiff(SECOND, '1970-01-01', profile_tag.modified) AS `cursor`");
        }

        $tagged->whereAdd('profile_tag.tagger = '.$this->tagger);
        $tagged->whereAdd("profile_tag.tag = '{$this->tag}'");

        if ($since != 0) {
            $tagged->whereAdd('cursor > ' . $since);
        }

        if ($upto != 0) {
            $tagged->whereAdd('cursor <= ' . $upto);
        }

        if ($limit != null) {
            $tagged->limit($offset, $limit);
        }

        $tagged->orderBy('profile_tag.modified DESC');
        $tagged->find();

        return $tagged;
    }

    /**
     * Gracefully delete one or many people tags
     * along with their members and subscriptions data
     *
     * @return boolean success
     */

    public function delete($useWhere = false)
    {
        // force delete one item at a time.
        if (empty($this->id)) {
            $this->find();
            while ($this->fetch()) {
                $this->delete();
            }
        }

        Profile_tag::cleanup($this);
        Profile_tag_subscription::cleanup($this);

        self::blow('profile:lists:%d', $this->tagger);

        return parent::delete($useWhere);
    }

    /**
     * Update a people tag gracefully
     * also change "tag" fields in profile_tag table
     *
     * @param Profile_list $dataObject    Object's original form
     *
     * @return boolean success
     */

    public function update($dataObject = false)
    {
        if (!is_object($dataObject) && !$dataObject instanceof Profile_list) {
            return parent::update($dataObject);
        }

        $result = true;

        // if original tag was different
        // check to see if the new tag already exists
        // if not, rename the tag correctly
        if ($dataObject->tag != $this->tag || $dataObject->tagger != $this->tagger) {
            $existing = Profile_list::getByTaggerAndTag($this->tagger, $this->tag);
            if (!empty($existing)) {
                // TRANS: Server exception.
                throw new ServerException(_('The tag you are trying to rename ' .
                                            'to already exists.'));
            }
            // move the tag
            // XXX: allow OStatus plugin to send out profile tag
            $result = Profile_tag::moveTag($dataObject, $this);
        }
        return parent::update($dataObject);
    }

    /**
     * return an xml string representing this people tag
     * as the author of an atom feed
     *
     * @return string atom author element
     */

    public function asAtomAuthor()
    {
        $xs = new XMLStringer(true);

        $tagger = $this->getTagger();
        $xs->elementStart('author');
        $xs->element('name', null, '@' . $tagger->nickname . '/' . $this->tag);
        $xs->element('uri', null, $this->permalink());
        $xs->elementEnd('author');

        return $xs->getString();
    }

    /**
     * return an xml string to represent this people tag
     * as a noun in an activitystreams feed.
     *
     * @param string $element the xml tag
     *
     * @return string activitystreams noun
     */

    public function asActivityNoun($element)
    {
        $noun = ActivityObject::fromPeopletag($this);
        return $noun->asString('activity:' . $element);
    }

    /**
     * get the cached number of profiles tagged with this
     * people tag, re-count if the argument is true.
     *
     * @param boolean $recount  whether to ignore cache
     *
     * @return integer count
     */

    public function taggedCount($recount = false)
    {
        $keypart = sprintf(
            'profile_list:tagged_count:%d:%s',
            $this->tagger,
            $this->tag
        );

        $count = self::cacheGet($keypart);

        if ($count === false) {
            $tags = new Profile_tag();

            $tags->tag = $this->tag;
            $tags->tagger = $this->tagger;

            $count = $tags->count('distinct tagged');

            self::cacheSet($keypart, $count);
        }

        return $count;
    }

    /**
     * get the cached number of profiles subscribed to this
     * people tag, re-count if the argument is true.
     *
     * @param boolean $recount  whether to ignore cache
     *
     * @return integer count
     */

    public function subscriberCount($recount = false)
    {
        $keypart = sprintf(
            'profile_list:subscriber_count:%d',
            $this->id
        );

        $count = self::cacheGet($keypart);

        if ($count === false) {
            $sub = new Profile_tag_subscription();
            $sub->profile_tag_id = $this->id;
            $count = (int) $sub->count('distinct profile_id');

            self::cacheSet($keypart, $count);
        }

        return $count;
    }

    /**
     * get the cached number of profiles subscribed to this
     * people tag, re-count if the argument is true.
     *
     * @param boolean $recount  whether to ignore cache
     *
     * @return integer count
     */

    public function blowNoticeStreamCache($all = false)
    {
        self::blow('profile_list:notice_ids:%d', $this->id);
        if ($all) {
            self::blow('profile_list:notice_ids:%d;last', $this->id);
        }
    }

    /**
     * get the Profile_list object by the
     * given tagger and with given tag
     *
     * @param integer $tagger   the id of the creator profile
     * @param integer $tag      the tag
     *
     * @return integer count
     */

    public static function getByTaggerAndTag($tagger, $tag)
    {
        $ptag = Profile_list::pkeyGet(array('tagger' => $tagger, 'tag' => $tag));
        return $ptag;
    }

    /**
     * create a profile_list record for a tag, tagger pair
     * if it doesn't exist, return it.
     *
     * @param integer $tagger   the tagger
     * @param string  $tag      the tag
     * @param string  $description description
     * @param boolean $private  protected or not
     *
     * @return Profile_list the people tag object
     */

    public static function ensureTag($tagger, $tag, $description = null, $private = false)
    {
        $ptag = Profile_list::getByTaggerAndTag($tagger, $tag);

        if (empty($ptag->id)) {
            $args = array(
                'tag' => $tag,
                'tagger' => $tagger,
                'description' => $description,
                'private' => $private
            );

            $new_tag = Profile_list::saveNew($args);

            return $new_tag;
        }
        return $ptag;
    }

    /**
     * get the maximum number of characters
     * that can be used in the description of
     * a people tag.
     *
     * determined by $config['peopletag']['desclimit']
     * if not set, falls back to $config['site']['textlimit']
     *
     * @return integer maximum number of characters
     */

    public static function maxDescription()
    {
        $desclimit = common_config('peopletag', 'desclimit');
        // null => use global limit (distinct from 0!)
        if (is_null($desclimit)) {
            $desclimit = common_config('site', 'textlimit');
        }
        return $desclimit;
    }

    /**
     * check if the length of given text exceeds
     * character limit.
     *
     * @param string $desc  the description
     *
     * @return boolean is the descripition too long?
     */

    public static function descriptionTooLong($desc)
    {
        $desclimit = self::maxDescription();
        return ($desclimit > 0 && !empty($desc) && (mb_strlen($desc) > $desclimit));
    }

    /**
     * save a new people tag, this should be always used
     * since it makes uri, homeurl, created and modified
     * timestamps and performs checks.
     *
     * @param array $fields an array with fields and their values
     *
     * @return mixed Profile_list on success, false on fail
     */
    public static function saveNew(array $fields)
    {
        extract($fields);

        $ptag = new Profile_list();

        $ptag->query('BEGIN');

        if (empty($tagger)) {
            // TRANS: Server exception saving new tag without having a tagger specified.
            throw new Exception(_('No tagger specified.'));
        }

        if (empty($tag)) {
            // TRANS: Server exception saving new tag without having a tag specified.
            throw new Exception(_('No tag specified.'));
        }

        if (empty($mainpage)) {
            $mainpage = null;
        }

        if (empty($uri)) {
            // fill in later...
            $uri = null;
        }

        if (empty($mainpage)) {
            $mainpage = null;
        }

        if (empty($description)) {
            $description = null;
        }

        if (empty($private)) {
            $private = false;
        }

        $ptag->tagger      = $tagger;
        $ptag->tag         = $tag;
        $ptag->description = $description;
        $ptag->private     = $private;
        $ptag->uri         = $uri;
        $ptag->mainpage    = $mainpage;
        $ptag->created     = common_sql_now();
        $ptag->modified    = common_sql_now();

        $result = $ptag->insert();

        if (!$result) {
            common_log_db_error($ptag, 'INSERT', __FILE__);
            // TRANS: Server exception saving new tag.
            throw new ServerException(_('Could not create profile tag.'));
        }

        if (!isset($uri) || empty($uri)) {
            $orig = clone($ptag);
            $ptag->uri = common_local_url('profiletagbyid', array('id' => $ptag->id, 'tagger_id' => $ptag->tagger));
            $result = $ptag->update($orig);
            if (!$result) {
                common_log_db_error($ptag, 'UPDATE', __FILE__);
                // TRANS: Server exception saving new tag.
                throw new ServerException(_('Could not set profile tag URI.'));
            }
        }

        if (!isset($mainpage) || empty($mainpage)) {
            $orig = clone($ptag);
            $user = User::getKV('id', $ptag->tagger);
            if (!empty($user)) {
                $ptag->mainpage = common_local_url('showprofiletag', array('tag' => $ptag->tag, 'nickname' => $user->getNickname()));
            } else {
                $ptag->mainpage = $uri; // assume this is a remote peopletag and the uri works
            }

            $result = $ptag->update($orig);
            if (!$result) {
                common_log_db_error($ptag, 'UPDATE', __FILE__);
                // TRANS: Server exception saving new tag.
                throw new ServerException(_('Could not set profile tag mainpage.'));
            }
        }
        return $ptag;
    }

    /**
     * get all items at given cursor position for api
     *
     * @param callback $fn  a function that takes the following arguments in order:
     *                      $offset, $limit, $since_id, $max_id
     *                      and returns a Profile_list object after making the DB query
     * @param array $args   arguments required for $fn
     * @param integer $cursor   the cursor
     * @param integer $count    max. number of results
     *
     * Algorithm:
     * - if cursor is 0, return empty list
     * - if cursor is -1, get first 21 items, next_cursor = 20th prev_cursor = 0
     * - if cursor is +ve get 22 consecutive items before starting at cursor
     *   - return items[1..20] if items[0] == cursor else return items[0..21]
     *   - prev_cursor = items[1]
     *   - next_cursor = id of the last item being returned
     *
     * - if cursor is -ve get 22 consecutive items after cursor starting at cursor
     *   - return items[1..20]
     *
     * @returns array (array (mixed items), int next_cursor, int previous_cursor)
     */

    // XXX: This should be in Memcached_DataObject... eventually

    public static function getAtCursor($fn, array $args, $cursor, $count = 20)
    {
        $items = array();

        $since_id = 0;
        $max_id = 0;
        $next_cursor = 0;
        $prev_cursor = 0;

        if ($cursor > 0) {
            // if cursor is +ve fetch $count+2 items before cursor starting at cursor
            $max_id = $cursor;
            $fn_args = array_merge($args, array(0, $count+2, 0, $max_id));
            $list = call_user_func_array($fn, $fn_args);
            while ($list->fetch()) {
                $items[] = clone($list);
            }

            if ((isset($items[0]->cursor) && $items[0]->cursor == $cursor) ||
                $items[0]->id == $cursor) {
                array_shift($items);
                $prev_cursor = isset($items[0]->cursor) ?
                    -$items[0]->cursor : -$items[0]->id;
            } else {
                if (count($items) > $count+1) {
                    array_shift($items);
                }
                // this means the cursor item has been deleted, check to see if there are more
                $fn_args = array_merge($args, array(0, 1, $cursor));
                $more = call_user_func($fn, $fn_args);
                if (!$more->fetch() || empty($more)) {
                    // no more items.
                    $prev_cursor = 0;
                } else {
                    $prev_cursor = isset($items[0]->cursor) ?
                        -$items[0]->cursor : -$items[0]->id;
                }
            }

            if (count($items)==$count+1) {
                // this means there is a next page.
                $next = array_pop($items);
                $next_cursor = isset($next->cursor) ?
                    $items[$count-1]->cursor : $items[$count-1]->id;
            }
        } elseif ($cursor < -1) {
            // if cursor is -ve fetch $count+2 items created after -$cursor-1
            $cursor = abs($cursor);
            $since_id = $cursor-1;

            $fn_args = array_merge($args, array(0, $count+2, $since_id));
            $list = call_user_func_array($fn, $fn_args);
            while ($list->fetch()) {
                $items[] = clone($list);
            }

            $end = count($items)-1;
            if ((isset($items[$end]->cursor) && $items[$end]->cursor == $cursor) ||
                $items[$end]->id == $cursor) {
                array_pop($items);
                $next_cursor = isset($items[$end-1]->cursor) ?
                    $items[$end-1]->cursor : $items[$end-1]->id;
            } else {
                $next_cursor = isset($items[$end]->cursor) ?
                    $items[$end]->cursor : $items[$end]->id;
                if ($end > $count) {
                    // excess item
                    array_pop($items);
                }

                // check if there are more items for next page
                $fn_args = array_merge($args, array(0, 1, 0, $cursor));
                $more = call_user_func_array($fn, $fn_args);
                if (!$more->fetch() || empty($more)) {
                    $next_cursor = 0;
                }
            }

            if (count($items) == $count+1) {
                // this means there is a previous page.
                $prev = array_shift($items);
                $prev_cursor = isset($prev->cursor) ?
                    -$items[0]->cursor : -$items[0]->id;
            }
        } elseif ($cursor == -1) {
            $fn_args = array_merge($args, array(0, $count+1));
            $list = call_user_func_array($fn, $fn_args);

            while ($list->fetch()) {
                $items[] = clone($list);
            }

            if (count($items)==$count+1) {
                $next = array_pop($items);
                if (isset($next->cursor)) {
                    $next_cursor = $items[$count-1]->cursor;
                } else {
                    $next_cursor = $items[$count-1]->id;
                }
            }
        }
        return array($items, $next_cursor, $prev_cursor);
    }

    /**
     * save a collection of people tags into the cache
     *
     * @param string $ckey  cache key
     * @param Profile_list &$tag the results to store
     * @param integer $offset   offset for slicing results
     * @param integer $limit    maximum number of results
     *
     * @return boolean success
     */

    public static function setCache($ckey, &$tag, $offset = 0, $limit = null)
    {
        $cache = Cache::instance();
        if (empty($cache)) {
            return false;
        }
        $str = '';
        $tags = array();
        while ($tag->fetch()) {
            $str .= $tag->tagger . ':' . $tag->tag . ';';
            $tags[] = clone($tag);
        }
        $str = substr($str, 0, -1);
        if ($offset>=0 && !is_null($limit)) {
            $tags = array_slice($tags, $offset, $limit);
        }

        $tag = new ArrayWrapper($tags);

        return self::cacheSet($ckey, $str);
    }

    /**
     * get people tags from the cache
     *
     * @param string $ckey  cache key
     * @param integer $offset   offset for slicing
     * @param integer $limit    limit
     *
     * @return Profile_list results
     */

    public static function getCached($ckey, $offset = 0, $limit = null)
    {
        $keys_str = self::cacheGet($ckey);
        if ($keys_str === false) {
            return false;
        }

        $pairs = explode(';', $keys_str);
        $keys = array();
        foreach ($pairs as $pair) {
            $keys[] = explode(':', $pair);
        }

        if ($offset>=0 && !is_null($limit)) {
            $keys = array_slice($keys, $offset, $limit);
        }
        return self::getByKeys($keys);
    }

    /**
     * get Profile_list objects from the database
     * given their (tag, tagger) key pairs.
     *
     * @param array $keys   array of array(tagger, tag)
     *
     * @return Profile_list results
     */

    public static function getByKeys(array $keys)
    {
        $cache = Cache::instance();

        if (!empty($cache)) {
            $tags = array();

            foreach ($keys as $key) {
                $t = Profile_list::getByTaggerAndTag($key[0], $key[1]);
                if (!empty($t)) {
                    $tags[] = $t;
                }
            }
            return new ArrayWrapper($tags);
        } else {
            $tag = new Profile_list();
            if (empty($keys)) {
                //if no IDs requested, just return the tag object
                return $tag;
            }

            $pairs = array();
            foreach ($keys as $key) {
                $pairs[] = '(' . $key[0] . ', "' . $key[1] . '")';
            }

            $tag->whereAdd('(tagger, tag) in (' . implode(', ', $pairs) . ')');

            $tag->find();

            $temp = array();

            while ($tag->fetch()) {
                $temp[$tag->tagger.'-'.$tag->tag] = clone($tag);
            }

            $wrapped = array();

            foreach ($keys as $key) {
                $id = $key[0].'-'.$key[1];
                if (array_key_exists($id, $temp)) {
                    $wrapped[] = $temp[$id];
                }
            }

            return new ArrayWrapper($wrapped);
        }
    }

    public function insert()
    {
        $result = parent::insert();
        if ($result) {
            self::blow('profile:lists:%d', $this->tagger);
        }
        return $result;
    }
}


code:
<?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/>.

/**
 * Data class for Profile preferences
 *
 * @category  Data
 * @package   GNUsocial
 * @author    Mikael Nordfeldth <mmn@hethane.se>
 * @copyright 2013 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

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

class Profile_prefs extends Managed_DataObject
{
    public $__table = 'profile_prefs';       // table name
    public $profile_id;                      // int(4)  primary_key not_null
    public $namespace;                       // varchar(191)  not_null
    public $topic;                           // varchar(191)  not_null
    public $data;                            // text
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'user'),
                'namespace' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'namespace, like pluginname or category'),
                'topic' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'preference key, i.e. description, age...'),
                'data' => array('type' => 'blob', 'description' => 'topic data, may be anything'),
                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
            ),
            'primary key' => array('profile_id', 'namespace', 'topic'),
            'foreign keys' => array(
                'profile_prefs_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array(
                'profile_prefs_profile_id_idx' => array('profile_id'),
            ),
        );
    }

    public static function getNamespacePrefs(Profile $profile, $namespace, array $topic = [])
    {
        if (empty($topic)) {
            $prefs = new Profile_prefs();
            $prefs->profile_id = $profile->getID();
            $prefs->namespace  = $namespace;
            $prefs->find();
        } else {
            $prefs = self::pivotGet('profile_id', $profile->getID(), array('namespace'=>$namespace, 'topic'=>$topic));
        }

        if (empty($prefs->N)) {
            throw new NoResultException($prefs);
        }

        return $prefs;
    }

    public static function getNamespace(Profile $profile, $namespace, array $topic = [])
    {
        $prefs = self::getNamespacePrefs($profile, $namespace, $topic);
        return $prefs->fetchAll();
    }

    public static function getAll(Profile $profile)
    {
        try {
            $prefs = self::listFind('profile_id', array($profile->getID()));
        } catch (NoResultException $e) {
            return array();
        }

        $list = array();
        while ($prefs->fetch()) {
            if (!isset($list[$prefs->namespace])) {
                $list[$prefs->namespace] = array();
            }
            $list[$prefs->namespace][$prefs->topic] = $prefs->data;
        }
        return $list;
    }

    public static function getTopic(Profile $profile, $namespace, $topic)
    {
        return Profile_prefs::getByPK(array('profile_id' => $profile->getID(),
                                            'namespace'  => $namespace,
                                            'topic'      => $topic));
    }

    public static function getData(Profile $profile, $namespace, $topic, $def = null)
    {
        try {
            $pref = self::getTopic($profile, $namespace, $topic);
        } catch (NoResultException $e) {
            if ($def === null) {
                // If no default value was set, continue the exception.
                throw $e;
            }
            // If there was a default value, return that.
            return $def;
        }
        return $pref->data;
    }

    public static function getConfigData(Profile $profile, $namespace, $topic)
    {
        try {
            $data = self::getData($profile, $namespace, $topic);
        } catch (NoResultException $e) {
            $data = common_config($namespace, $topic);
        }
        return $data;
    }

    /*
     * Sets a profile preference based on Profile, namespace and topic
     *
     * @param  Profile $profile   Which profile this is for
     * @param  string  $namespace Under which namespace (pluginname etc.)
     * @param  string  $topic     Preference name (think key in key-val store)
     * @param  string  $data      Data to be put into preference storage, null means delete
     *
     * @return true if changes are made, false if no action taken
     * @throws ServerException if preference could not be saved
     */
    public static function setData(Profile $profile, $namespace, $topic, $data = null)
    {
        try {
            $pref = self::getTopic($profile, $namespace, $topic);
            if (is_null($data)) {
                $pref->delete();
            } else {
                $orig = clone($pref);
                $pref->data = DB_DataObject_Cast::blob($data);
                $pref->update($orig);
            }
            return true;
        } catch (NoResultException $e) {
            if (is_null($data)) {
                return false; // No action taken
            }
        }

        $pref = new Profile_prefs();
        $pref->profile_id = $profile->getID();
        $pref->namespace  = $namespace;
        $pref->topic      = $topic;
        $pref->data       = DB_DataObject_Cast::blob($data);
        $pref->created    = common_sql_now();

        if ($pref->insert() === false) {
            throw new ServerException('Could not save profile preference.');
        }
        return true;
    }
}

repo_name: suraj/sloth

license: mit


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2009, StatusNet, Inc.
 *
 * 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
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Table Definition for profile_role
 */

require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Profile_role extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'profile_role';                    // table name
    public $profile_id;                      // int(4)  primary_key not_null
    public $role;                            // varchar(32)  primary_key not_null
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'account having the role'),
                'role' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'string representing the role'),
                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date the role was granted'),
            ),
            'primary key' => array('profile_id', 'role'),
            'foreign keys' => array(
                'profile_role_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array('profile_role_role_created_profile_id_idx' => array('role', 'created', 'profile_id')),
        );
    }

    const OWNER         = 'owner';
    const MODERATOR     = 'moderator';
    const ADMINISTRATOR = 'administrator';
    const SANDBOXED     = 'sandboxed';
    const SILENCED      = 'silenced';
    const DELETED       = 'deleted'; // Pending final deletion of notices...

    public static function isValid($role)
    {
        // @fixme could probably pull this from class constants
        $known = array(self::OWNER,
                       self::MODERATOR,
                       self::ADMINISTRATOR,
                       self::SANDBOXED,
                       self::SILENCED);
        return in_array($role, $known);
    }

    public static function isSettable($role)
    {
        $allowedRoles = array('administrator', 'moderator');
        return self::isValid($role) && in_array($role, $allowedRoles);
    }
}


code:
<?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/>.

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

/**
 * Table Definition for profile_tag
 */
class Profile_tag extends Managed_DataObject
{
    public $__table = 'profile_tag';                     // table name
    public $tagger;                          // int(4)  primary_key not_null
    public $tagged;                          // int(4)  primary_key not_null
    public $tag;                             // varchar(64)  primary_key not_null
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    public static function schemaDef()
    {
        return array(

            'fields' => array(
                'tagger' => array('type' => 'int', 'not null' => true, 'description' => 'user making the tag'),
                'tagged' => array('type' => 'int', 'not null' => true, 'description' => 'profile tagged'),
                'tag' => array('type' => 'varchar', 'length' => 64, 'not null' => true, 'description' => 'hash tag associated with this notice'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date the tag was added'),
            ),
            'primary key' => array('tagger', 'tagged', 'tag'),
            'foreign keys' => array(
                'profile_tag_tagger_fkey' => array('profile', array('tagger' => 'id')),
                'profile_tag_tagged_fkey' => array('profile', array('tagged' => 'id')),
            ),
            'indexes' => array(
                'profile_tag_modified_idx' => array('modified'),
                'profile_tag_tagger_tag_idx' => array('tagger', 'tag'),
                'profile_tag_tagged_idx' => array('tagged'),
            ),
        );
    }

    public function links()
    {
        return array('tagger,tag' => 'profile_list:tagger,tag');
    }

    public function getMeta()
    {
        return Profile_list::pkeyGet(array('tagger' => $this->tagger, 'tag' => $this->tag));
    }

    public static function getSelfTagsArray(Profile $target)
    {
        return self::getTagsArray($target->getID(), $target->getID(), $target);
    }

    public static function setSelfTags(Profile $target, array $newtags, array $privacy = [])
    {
        return self::setTags($target->getID(), $target->getID(), $newtags, $privacy);
    }

    public static function getTags($tagger, $tagged, $auth_user = null)
    {
        $profile_list = new Profile_list();
        $include_priv = 1;

        if (!($auth_user instanceof User ||
            $auth_user instanceof Profile) ||
            ($auth_user->id !== $tagger)) {
            $profile_list->private = false;
            $include_priv = 0;
        }

        $key = sprintf('profile_tag:tagger_tagged_privacy:%d-%d-%d', $tagger, $tagged, $include_priv);
        $tags = Profile_list::getCached($key);
        if ($tags !== false) {
            return $tags;
        }

        $qry = 'select profile_list.* from profile_list left join '.
               'profile_tag on (profile_list.tag = profile_tag.tag and '.
               'profile_list.tagger = profile_tag.tagger) where '.
               'profile_tag.tagger = %d and profile_tag.tagged = %d ';
        $qry = sprintf($qry, $tagger, $tagged);

        if (!$include_priv) {
            $qry .= ' AND profile_list.private = FALSE';
        }

        $profile_list->query($qry);

        Profile_list::setCache($key, $profile_list);

        return $profile_list;
    }

    public static function getTagsArray($tagger, $tagged, Profile $scoped = null)
    {
        $ptag = new Profile_tag();

        $qry = sprintf(
            'SELECT profile_tag.tag '.
            'FROM profile_tag INNER JOIN profile_list '.
            ' ON (profile_tag.tagger = profile_list.tagger ' .
            '     and profile_tag.tag = profile_list.tag) ' .
            'WHERE profile_tag.tagger = %d ' .
            'AND   profile_tag.tagged = %d ',
            $tagger,
            $tagged
        );

        if (!$scoped instanceof Profile || $scoped->getID() !== $tagger) {
            $qry .= 'AND profile_list.private = FALSE';
        }

        $tags = array();

        $ptag->query($qry);

        while ($ptag->fetch()) {
            $tags[] = $ptag->tag;
        }

        return $tags;
    }

    public static function setTags($tagger, $tagged, array $newtags, array $privacy = [])
    {
        $newtags = array_unique($newtags);
        $oldtags = self::getTagsArray($tagger, $tagged, Profile::getByID($tagger));

        $ptag = new Profile_tag();

        // Delete stuff that's in old and not in new

        $to_delete = array_diff($oldtags, $newtags);

        // Insert stuff that's in new and not in old

        $to_insert = array_diff($newtags, $oldtags);

        foreach ($to_delete as $deltag) {
            self::unTag($tagger, $tagged, $deltag);
        }

        foreach ($to_insert as $instag) {
            $private = isset($privacy[$instag]) ? $privacy[$instag] : false;
            self::setTag($tagger, $tagged, $instag, null, $private);
        }
        return true;
    }

    # set a single tag
    public static function setTag($tagger, $tagged, $tag, $desc=null, $private = false)
    {
        $ptag = Profile_tag::pkeyGet(array('tagger' => $tagger,
                                           'tagged' => $tagged,
                                           'tag' => $tag));

        # if tag already exists, return it
        if ($ptag instanceof Profile_tag) {
            return $ptag;
        }

        $tagger_profile = Profile::getByID($tagger);
        $tagged_profile = Profile::getByID($tagged);

        if (Event::handle('StartTagProfile', array($tagger_profile, $tagged_profile, $tag))) {
            if (!$tagger_profile->canTag($tagged_profile)) {
                // TRANS: Client exception thrown trying to set a tag for a user that cannot be tagged.
                throw new ClientException(_('You cannot tag this user.'));
            }

            $tags = new Profile_list();
            $tags->tagger = $tagger;
            $count = (int) $tags->count('distinct tag');

            if ($count >= common_config('peopletag', 'maxtags')) {
                // TRANS: Client exception thrown trying to set more tags than allowed.
                throw new ClientException(sprintf(
                    _('You already have created %d or more tags ' .
                      'which is the maximum allowed number of tags. ' .
                      'Try using or deleting some existing tags.'),
                    common_config('peopletag', 'maxtags')
                ));
            }

            $plist = new Profile_list();
            $plist->query('BEGIN');

            $profile_list = Profile_list::ensureTag($tagger, $tag, $desc, $private);

            if ($profile_list->taggedCount() >= common_config('peopletag', 'maxpeople')) {
                // TRANS: Client exception thrown when trying to add more people than allowed to a list.
                throw new ClientException(sprintf(
                    _('You already have %1$d or more people in list %2$s, ' .
                      'which is the maximum allowed number. ' .
                      'Try unlisting others first.'),
                    common_config('peopletag', 'maxpeople'),
                    $tag
                ));
            }

            $newtag = new Profile_tag();

            $newtag->tagger = $tagger;
            $newtag->tagged = $tagged;
            $newtag->tag = $tag;

            $result = $newtag->insert();

            if (!$result) {
                common_log_db_error($newtag, 'INSERT', __FILE__);
                $plist->query('ROLLBACK');
                return false;
            }

            try {
                $plist->query('COMMIT');
                Event::handle('EndTagProfile', array($newtag));
            } catch (Exception $e) {
                $newtag->delete();
                $profile_list->delete();
                throw $e;
            }

            $profile_list->taggedCount(true);
            self::blowCaches($tagger, $tagged);
        }

        return $newtag;
    }

    public static function unTag($tagger, $tagged, $tag)
    {
        $ptag = Profile_tag::pkeyGet(array('tagger' => $tagger,
                                           'tagged' => $tagged,
                                           'tag'    => $tag));
        if (!$ptag) {
            return true;
        }

        if (Event::handle('StartUntagProfile', array($ptag))) {
            $orig = clone($ptag);
            $result = $ptag->delete();
            if ($result === false) {
                common_log_db_error($this, 'DELETE', __FILE__);
                return false;
            }
            Event::handle('EndUntagProfile', array($orig));
            $profile_list = Profile_list::pkeyGet(array('tag' => $tag, 'tagger' => $tagger));
            if (!empty($profile_list)) {
                $profile_list->taggedCount(true);
            }
            self::blowCaches($tagger, $tagged);
            return true;
        }
    }

    // @fixme: move this to Profile_list?
    public static function cleanup($profile_list)
    {
        $ptag = new Profile_tag();
        $ptag->tagger = $profile_list->tagger;
        $ptag->tag = $profile_list->tag;
        $ptag->find();

        while ($ptag->fetch()) {
            if (Event::handle('StartUntagProfile', array($ptag))) {
                $orig = clone($ptag);
                $result = $ptag->delete();
                if (!$result) {
                    common_log_db_error($this, 'DELETE', __FILE__);
                }
                Event::handle('EndUntagProfile', array($orig));
            }
        }
    }

    // move a tag!
    public static function moveTag($orig, $new)
    {
        $tags = new Profile_tag();
        $qry = "UPDATE profile_tag SET tag = '%s', tagger = '%s' " .
               "WHERE tag = '%s' AND tagger = '%s'";
        $result = $tags->query(sprintf(
            $qry,
            $tags->escape($new->tag),
            $tags->escape($new->tagger),
            $tags->escape($orig->tag),
            $tags->escape($orig->tagger)
        ));

        if ($result === false) {
            common_log_db_error($tags, 'UPDATE', __FILE__);
            throw new Exception('Could not move Profile_tag, see db log for details.');
        }
        return $result;
    }

    public static function blowCaches($tagger, $tagged)
    {
        foreach (array(0, 1) as $perm) {
            self::blow(sprintf('profile_tag:tagger_tagged_privacy:%d-%d-%d', $tagger, $tagged, $perm));
        }
        return true;
    }

    // Return profiles with a given tag
    public static function getTagged($tagger, $tag)
    {
        $profile = new Profile();
        $profile->query('SELECT profile.* ' .
                        'FROM profile JOIN profile_tag ' .
                        'ON profile.id = profile_tag.tagged ' .
                        'WHERE profile_tag.tagger = ' . $profile->escape($tagger) . ' ' .
                        "AND profile_tag.tag = '" . $profile->escape($tag) . "' ");
        $tagged = [];
        while ($profile->fetch()) {
            $tagged[] = clone($profile);
        }
        return true;
    }

    public function insert()
    {
        $result = parent::insert();
        if ($result) {
            self::blow(
                'profile_list:tagged_count:%d:%s',
                $this->tagger,
                $this->tag
            );
        }
        return $result;
    }

    public function delete($useWhere = false)
    {
        $result = parent::delete($useWhere);
        if ($result !== false) {
            self::blow(
                'profile_list:tagged_count:%d:%s',
                $this->tagger,
                $this->tag
            );
        }
        return $result;
    }
}


code:
<?php
/**
 * Table Definition for profile_tag_subscription
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Profile_tag_subscription extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'profile_tag_subscription';                     // table name
    public $profile_tag_id;                         // int(4)  not_null
    public $profile_id;                             // int(4)  not_null
    public $created;                                // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                               // datetime()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'profile_tag_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile_tag'),
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to profile table'),

                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
            ),
            'primary key' => array('profile_tag_id', 'profile_id'),
            'foreign keys' => array(
                'profile_tag_subscription_profile_list_id_fkey' => array('profile_list', array('profile_tag_id' => 'id')),
                'profile_tag_subscription_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array(
                // @fixme probably we want a (profile_id, created) index here?
                'profile_tag_subscription_profile_id_idx' => array('profile_id'),
                'profile_tag_subscription_created_idx' => array('created'),
            ),
        );
    }

    static function add($peopletag, $profile)
    {
        if ($peopletag->private) {
            return false;
        }

        if (Event::handle('StartSubscribePeopletag', array($peopletag, $profile))) {
            $args = array('profile_tag_id' => $peopletag->id,
                          'profile_id' => $profile->id);
            $existing = Profile_tag_subscription::pkeyGet($args);
            if(!empty($existing)) {
                return $existing;
            }

            $sub = new Profile_tag_subscription();
            $sub->profile_tag_id = $peopletag->id;
            $sub->profile_id = $profile->id;
            $sub->created = common_sql_now();

            $result = $sub->insert();

            if (!$result) {
                common_log_db_error($sub, 'INSERT', __FILE__);
                // TRANS: Exception thrown when inserting a list subscription in the database fails.
                throw new Exception(_('Adding list subscription failed.'));
            }

            $ptag = Profile_list::getKV('id', $peopletag->id);
            $ptag->subscriberCount(true);

            Event::handle('EndSubscribePeopletag', array($peopletag, $profile));
            return $ptag;
        }
    }

    static function remove($peopletag, $profile)
    {
        $sub = Profile_tag_subscription::pkeyGet(array('profile_tag_id' => $peopletag->id,
                                              'profile_id' => $profile->id));

        if (empty($sub)) {
            // silence is golden?
            return true;
        }

        if (Event::handle('StartUnsubscribePeopletag', array($peopletag, $profile))) {
            $result = $sub->delete();

            if (!$result) {
                common_log_db_error($sub, 'DELETE', __FILE__);
                // TRANS: Exception thrown when deleting a list subscription from the database fails.
                throw new Exception(_('Removing list subscription failed.'));
            }

            $peopletag->subscriberCount(true);

            Event::handle('EndUnsubscribePeopletag', array($peopletag, $profile));
            return true;
        }
    }

    // called if a tag gets deleted / made private
    static function cleanup($profile_list) {
        $subs = new self();
        $subs->profile_tag_id = $profile_list->id;
        $subs->find();

        while($subs->fetch()) {
            $profile = Profile::getKV('id', $subs->profile_id);
            Event::handle('StartUnsubscribePeopletag', array($profile_list, $profile));
            // Delete anyway
            $subs->delete();
            Event::handle('StartUnsubscribePeopletag', array($profile_list, $profile));
        }
    }

    function insert()
    {
        $result = parent::insert();
        if ($result) {
            self::blow('profile_list:subscriber_count:%d',
                       $this->profile_tag_id);
        }
        return $result;
    }

    function delete($useWhere=false)
    {
        $result = parent::delete($useWhere);
        if ($result !== false) {
            self::blow('profile_list:subscriber_count:%d',
                       $this->profile_tag_id);
        }
        return $result;
    }
}


code:
<?php
/**
 * Table Definition for queue_item
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Queue_item extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'queue_item';                      // table name
    public $id;                              // int(4)  primary_key not_null
    public $frame;                           // blob not_null
    public $transport;                       // varchar(32)
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $claimed;                         // datetime()

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'id' => array('type' => 'serial', 'not null' => true, 'description' => 'unique identifier'),
                'frame' => array('type' => 'blob', 'not null' => true, 'description' => 'data: object reference or opaque string'),
                'transport' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'queue for what? "email", "xmpp", "sms", "irc", ...'),
                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'),
                'claimed' => array('type' => 'datetime', 'description' => 'date this item was claimed'),
            ),
            'primary key' => array('id'),
            'indexes' => array(
                'queue_item_created_idx' => array('created'),
            ),
        );
    }

    /**
     * @param mixed $transports name of a single queue or array of queues to pull from
     *                          If not specified, checks all queues in the system.
     */
    static function top($transports=null, array $ignored_transports=array()) {

        $qi = new Queue_item();
        if ($transports) {
            if (is_array($transports)) {
                // @fixme use safer escaping
                $list = implode("','", array_map(array($qi, 'escape'), $transports));
                $qi->whereAdd("transport in ('$list')");
            } else {
                $qi->transport = $transports;
            }
        }
        if (!empty($ignored_transports)) {
            // @fixme use safer escaping
            $list = implode("','", array_map(array($qi, 'escape'), $ignored_transports));
            $qi->whereAdd("transport NOT IN ('$list')");
        }
        $qi->orderBy('created');
        $qi->whereAdd('claimed is null');

        $qi->limit(1);

        $cnt = $qi->find(true);

        if ($cnt) {
            // XXX: potential race condition
            // can we force it to only update if claimed is still null
            // (or old)?
            common_log(LOG_INFO, 'claiming queue item id = ' . $qi->getID() . ' for transport ' . $qi->transport);
            $orig = clone($qi);
            $qi->claimed = common_sql_now();
            $result = $qi->update($orig);
            if ($result) {
                common_log(LOG_DEBUG, 'claim succeeded.');
                return $qi;
            } else {
                common_log(LOG_ERR, 'claim of queue item id= ' . $qi->getID() . ' for transport ' . $qi->transport . ' failed.');
            }
        }
        $qi = null;
        return null;
    }

    /**
     * Release a claimed item.
     */
    function releaseClaim()
    {
        // DB_DataObject doesn't let us save nulls right now
        $sql = sprintf("UPDATE queue_item SET claimed=NULL WHERE id=%d", $this->getID());
        $this->query($sql);

        $this->claimed = null;
        $this->encache();
    }
}


code:
<?php
/**
 * Table Definition for related_group
 */

class Related_group extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'related_group';                   // table name
    public $group_id;                        // int(4)  primary_key not_null
    public $related_group_id;                // int(4)  primary_key not_null
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            // @fixme description for related_group?
            'fields' => array(
                'group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'),
                'related_group_id' => array('type' => 'int', 'not null' => true, 'description' => 'foreign key to user_group'),
                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'),
            ),
            'primary key' => array('group_id', 'related_group_id'),
            'foreign keys' => array(
                'related_group_group_id_fkey' => array('user_group', array('group_id' => 'id')),
                'related_group_related_group_id_fkey' => array('user_group', array('related_group_id' => 'id')),
            ),
        );
    }
}


code:
<?php
/**
 * Table Definition for remember_me
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Remember_me extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'remember_me';                     // table name
    public $code;                            // varchar(32)  primary_key not_null
    public $user_id;                         // int(4)   not_null
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'code' => array('type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'good random code'),
                'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who is logged in'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
            ),
            'primary key' => array('code'),
            'foreign keys' => array(
                'remember_me_user_id_fkey' => array('user', array('user_id' => 'id')),
            ),
        );
    }
}


code:
<?php
/**
 * Table Definition for reply
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Reply extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'reply';                           // table name
    public $notice_id;                       // int(4)  primary_key not_null
    public $profile_id;                      // int(4)  primary_key not_null
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP
    public $replied_id;                      // int(4)

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice that is the reply'),
                'profile_id' => array('type' => 'int', 'not null' => true, 'description' => 'profile replied to'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
                'replied_id' => array('type' => 'int', 'description' => 'notice replied to (not used, see notice.reply_to)'),
            ),
            'primary key' => array('notice_id', 'profile_id'),
            'foreign keys' => array(
                'reply_notice_id_fkey' => array('notice', array('notice_id' => 'id')),
                'reply_profile_id_fkey' => array('profile', array('profile_id' => 'id')),
            ),
            'indexes' => array(
                'reply_notice_id_idx' => array('notice_id'),
                'reply_profile_id_idx' => array('profile_id'),
                'reply_replied_id_idx' => array('replied_id'),
                'reply_profile_id_modified_notice_id_idx' => array('profile_id', 'modified', 'notice_id')
            ),
        );
    }

    /**
     * Wrapper for record insertion to update related caches
     */
    function insert()
    {
        $result = parent::insert();

        if ($result) {
            self::blow('reply:stream:%d', $this->profile_id);
        }

        return $result;
    }

    static function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0)
    {
        // FIXME: Use some other method to get Profile::current() in order
        // to avoid confusion between background processing and session user.
        $stream = new ReplyNoticeStream($user_id, Profile::current());
        return $stream->getNotices($offset, $limit, $since_id, $max_id);
    }
}


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * 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
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * Extended DB_DataObject to improve a few things:
 * - free global resources from destructor
 * - remove bogus global references from serialized objects
 * - don't leak memory when loading already-used .ini files
 *   (eg when using the same schema on thousands of databases)
 */
class Safe_DataObject extends GS_DataObject
{
    /**
     * Destructor to free global memory resources associated with
     * this data object when it's unset or goes out of scope.
     * DB_DataObject doesn't do this yet by itself.
     */

    function __destruct()
    {
        $this->free();
        if (method_exists('DB_DataObject', '__destruct')) {
            parent::__destruct();
        }
    }

    /**
     * Magic function called at clone() time.
     *
     * We use this to drop connection with some global resources.
     * This supports the fairly common pattern where individual
     * items being read in a loop via a single object are cloned
     * for individual processing, then fall out of scope when the
     * loop comes around again.
     *
     * As that triggers the destructor, we want to make sure that
     * the original object doesn't have its database result killed.
     * It will still be freed properly when the original object
     * gets destroyed.
     */
    function __clone()
    {
        $this->_DB_resultid = false;
    }

    /**
     * Magic function called at serialize() time.
     *
     * We use this to drop a couple process-specific references
     * from DB_DataObject which can cause trouble in future
     * processes.
     *
     * @return array of variable names to include in serialization.
     */
    function __sleep()
    {
        $vars = array_keys(get_object_vars($this));
        $skip = array('_DB_resultid', '_link_loaded');
        return array_diff($vars, $skip);
    }

    /**
     * Magic function called at unserialize() time.
     *
     * Clean out some process-specific variables which might
     * be floating around from a previous process's cached
     * objects.
     *
     * Old cached objects may still have them.
     */
    function __wakeup()
    {
        // Refers to global state info from a previous process.
        // Clear this out so we don't accidentally break global
        // state in *this* process.
        $this->_DB_resultid = null;
        // We don't have any local DBO refs, so clear these out.
        $this->_link_loaded = false;
    }

    /**
     * Magic function called when someone attempts to call a method
     * that doesn't exist. DB_DataObject uses this to implement
     * setters and getters for fields, but neglects to throw an error
     * when you just misspell an actual method name. This leads to
     * silent failures which can cause all kinds of havoc.
     *
     * @param string $method
     * @param array $params
     * @return mixed
     * @throws Exception
     */
    function __call($method, $params)
    {
        $return = null;
        // Yes, that's _call with one underscore, which does the
        // actual implementation.
        if ($this->_call($method, $params, $return)) {
            return $return;
        } else {
            // Low level exception. No need for i18n as discussed with Brion.
            throw new Exception('Call to undefined method ' .
                get_class($this) . '::' . $method);
        }
    }

    /**
     * Work around memory-leak bugs...
     * Had to copy-paste the whole function in order to patch a couple lines of it.
     * Would be nice if this code was better factored.
     *
     * @param optional string  name of database to assign / read
     * @param optional array   structure of database, and keys
     * @param optional array  table links
     *
     * @access public
     * @return true or PEAR:error on wrong paramenters.. or false if no file exists..
     *              or the array(tablename => array(column_name=>type)) if called with 1 argument.. (databasename)
     */
    function databaseStructure()
    {
        global $_DB_DATAOBJECT;

        // Assignment code

        if ($args = func_get_args()) {

            if (count($args) == 1) {

                // this returns all the tables and their structure..
                if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
                    $this->debug("Loading Generator as databaseStructure called with args",1);
                }

                $x = new DB_DataObject;
                $x->_database = $args[0];
                $this->_connect();
                $DB = &$_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5];

                $tables = $DB->getListOf('tables');
                class_exists('DB_DataObject_Generator') ? '' :
                    require_once 'DB/DataObject/Generator.php';

                foreach($tables as $table) {
                    $y = new DB_DataObject_Generator;
                    $y->fillTableSchema($x->_database,$table);
                }
                return $_DB_DATAOBJECT['INI'][$x->_database];
            } else {

                $_DB_DATAOBJECT['INI'][$args[0]] = isset($_DB_DATAOBJECT['INI'][$args[0]]) ?
                    $_DB_DATAOBJECT['INI'][$args[0]] + $args[1] : $args[1];

                if (isset($args[1])) {
                    $_DB_DATAOBJECT['LINKS'][$args[0]] = isset($_DB_DATAOBJECT['LINKS'][$args[0]]) ?
                        $_DB_DATAOBJECT['LINKS'][$args[0]] + $args[2] : $args[2];
                }
                return true;
            }

        }
        if (!$this->_database) {
            $this->_connect();
        }

        // loaded already?
        if (!empty($_DB_DATAOBJECT['INI'][$this->_database])) {

            // database loaded - but this is table is not available..
            if (
                    empty($_DB_DATAOBJECT['INI'][$this->_database][$this->tableName()])
                    && !empty($_DB_DATAOBJECT['CONFIG']['proxy'])
                ) {
                if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
                    $this->debug("Loading Generator to fetch Schema",1);
                }
                class_exists('DB_DataObject_Generator') ? '' :
                    require_once 'DB/DataObject/Generator.php';

                $x = new DB_DataObject_Generator;
                $x->fillTableSchema($this->_database,$this->tableName());
            }
            return true;
        }

        if (empty($_DB_DATAOBJECT['CONFIG'])) {
            self::_loadConfig();
        }

        // if you supply this with arguments, then it will take those
        // as the database and links array...

        $schemas = isset($_DB_DATAOBJECT['CONFIG']['schema_location']) ?
            array("{$_DB_DATAOBJECT['CONFIG']['schema_location']}/{$this->_database}.ini") :
            array() ;

        if (isset($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"])) {
            $schemas = is_array($_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]) ?
                $_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"] :
                explode(PATH_SEPARATOR,$_DB_DATAOBJECT['CONFIG']["ini_{$this->_database}"]);
        }

        /* BEGIN CHANGED FROM UPSTREAM */
        $_DB_DATAOBJECT['INI'][$this->_database] = $this->parseIniFiles($schemas);
        /* END CHANGED FROM UPSTREAM */

        // now have we loaded the structure..

        if (!empty($_DB_DATAOBJECT['INI'][$this->_database][$this->tableName()])) {
            return true;
        }
        // - if not try building it..
        if (!empty($_DB_DATAOBJECT['CONFIG']['proxy'])) {
            class_exists('DB_DataObject_Generator') ? '' :
                require_once 'DB/DataObject/Generator.php';

            $x = new DB_DataObject_Generator;
            $x->fillTableSchema($this->_database,$this->tableName());
            // should this fail!!!???
            return true;
        }
        $this->debug("Cant find database schema: {$this->_database}/{$this->tableName()} \n".
                    "in links file data: " . print_r($_DB_DATAOBJECT['INI'],true),"databaseStructure",5);
        // we have to die here!! - it causes chaos if we don't (including looping forever!)
        // Low level exception. No need for i18n as discussed with Brion.
        $this->raiseError( "Unable to load schema for database and table (turn debugging up to 5 for full error message)", DB_DATAOBJECT_ERROR_INVALIDARGS, PEAR_ERROR_DIE);
        return false;
    }

    /** For parseIniFiles */
    protected static $iniCache = array();

    /**
     * When switching site configurations, DB_DataObject was loading its
     * .ini files over and over, leaking gobs of memory.
     * This refactored helper function uses a local cache of .ini files
     * to minimize the leaks.
     *
     * @param array of .ini file names $schemas
     * @return array
     */
    protected function parseIniFiles(array $schemas)
    {
        $key = implode("|", $schemas);
        if (!isset(Safe_DataObject::$iniCache[$key])) {
            $data = array();
            foreach ($schemas as $ini) {
                if (file_exists($ini) && is_file($ini)) {
                    $data = array_merge($data, parse_ini_file($ini, true));

                    if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
                        if (!is_readable ($ini)) {
                            $this->debug("ini file is not readable: $ini","databaseStructure",1);
                        } else {
                            $this->debug("Loaded ini file: $ini","databaseStructure",1);
                        }
                    }
                } else {
                    if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) {
                        $this->debug("Missing ini file: $ini","databaseStructure",1);
                    }
                }
            }
            Safe_DataObject::$iniCache[$key] = $data;
        }

        return Safe_DataObject::$iniCache[$key];
    }
}


code:
<?php
/**
 * Table Definition for schema_version
 */

class Schema_version extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'schema_version';      // table name
    public $table_name;                      // varchar(64)  primary_key not_null
    public $checksum;                        // varchar(64)  not_null
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    public static function schemaDef()
    {
        return array(
            'description' => 'To avoid checking database structure all the time, we store a checksum of the expected schema info for each table here. If it has not changed since the last time we checked the table, we can leave it as is.',
            'fields' => array(
                'table_name' => array('type' => 'varchar', 'length' => '64', 'not null' => true, 'description' => 'Table name'),
                'checksum' => array('type' => 'varchar', 'length' => '64', 'not null' => true, 'description' => 'Checksum of schema array; a mismatch indicates we should check the table more thoroughly.'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
            ),
            'primary key' => array('table_name'),
        );
    }
}


code:
<?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/>.

/**
 * Table Definition for session
 *
 * @package   GNUsocial
 * @author    Evan Prodromou
 * @author    Brion Vibber
 * @author    Mikael Nordfeldth
 * @author    Sorokin Alexei
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 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();

require_once INSTALLDIR . '/classes/Memcached_DataObject.php';

/**
 * Superclass representing a saved session as it exists in the database.
 *
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Session extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'session';             // table name
    public $id;                              // varchar(32)  primary_key not_null
    public $session_data;                    // text()
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // datetime()  not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    /**
     * Returns an array describing how the session is stored in the database.
     *
     * @return array
     */
    public static function schemaDef()
    {
        return [
            'fields' => [
                'id' => ['type' => 'varchar', 'length' => 32, 'not null' => true, 'description' => 'session ID'],
                'session_data' => ['type' => 'text', 'description' => 'session data'],
                'created' => ['type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'],
                'modified' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
            ],
            'primary key' => ['id'],
            'indexes' => [
                'session_modified_idx' => ['modified'],
            ],
        ];
    }

    /**
     * New code should NOT call this function.
     * Dummy function for backwards compatibility with older plugins like Qvitter.
     * Stuff to do before the request teardown.
     *
     * @return void
     */
    public static function cleanup()
    {
        session_write_close();
    }
}


code:
<?php
/**
 * Table Definition for sms_carrier
 */
require_once INSTALLDIR.'/classes/Memcached_DataObject.php';

class Sms_carrier extends Managed_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'sms_carrier';                     // table name
    public $id;                              // int(4)  primary_key not_null
    public $name;                            // varchar(64)  unique_key
    public $email_pattern;                   // varchar(191)   not_null   not 255 because utf8mb4 takes more space
    public $created;                         // datetime()   not_null default_0000-00-00%2000%3A00%3A00
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    function toEmailAddress($sms)
    {
        return sprintf($this->email_pattern, $sms);
    }

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'id' => array('type' => 'int', 'not null' => true, 'description' => 'primary key for SMS carrier'),
                'name' => array('type' => 'varchar', 'length' => 64, 'description' => 'name of the carrier'),
                'email_pattern' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'sprintf pattern for making an email address from a phone number'),
                'created' => array('type' => 'datetime', 'not null' => true, 'default' => '0000-00-00 00:00:00', 'description' => 'date this record was created'),
                'modified' => array('type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'),
            ),
            'primary key' => array('id'),
            'unique keys' => array(
                'sms_carrier_name_key' => array('name'),
            ),
        );
    }
}


code:
<?php
/**
 * Table Definition for status_network
 *
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2009, StatusNet, Inc.
 *
 * 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
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }

class Status_network extends Safe_DataObject
{
    ###START_AUTOCODE
    /* the code below is auto generated do not remove the above tag */

    public $__table = 'status_network';                  // table name
    public $site_id;                         // int(4) primary_key not_null
    public $nickname;                        // varchar(64)   unique_key not_null
    public $hostname;                        // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $pathname;                        // varchar(191)  unique_key   not 255 because utf8mb4 takes more space
    public $dbhost;                          // varchar(191)               not 255 because utf8mb4 takes more space
    public $dbuser;                          // varchar(191)               not 255 because utf8mb4 takes more space
    public $dbpass;                          // varchar(191)               not 255 because utf8mb4 takes more space
    public $dbname;                          // varchar(191)               not 255 because utf8mb4 takes more space
    public $sitename;                        // varchar(191)               not 255 because utf8mb4 takes more space
    public $theme;                           // varchar(191)               not 255 because utf8mb4 takes more space
    public $logo;                            // varchar(191)               not 255 because utf8mb4 takes more space
    public $created;                         // datetime()   not_null
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    /* Static get */
    static function getKV($k,$v=NULL) {
        // TODO: This must probably be turned into a non-static call
        $i = DB_DataObject::staticGet('Status_network',$k,$v);

        // Don't use local process cache; if we're fetching multiple
        // times it's because we're reloading it in a long-running
        // process; we need a fresh copy!
        global $_DB_DATAOBJECT;
        unset($_DB_DATAOBJECT['CACHE']['status_network']);
        return $i;
    }

    /* the code above is auto generated do not remove the tag below */
    ###END_AUTOCODE

    // XXX: made public so Status_network_tag can eff with it
    public static $cache = null;
    public static $cacheInitialized = false;
    static $base = null;
    static $wildcard = null;

    /**
     * @param string $dbhost
     * @param string $dbuser
     * @param string $dbpass
     * @param string $dbname
     * @param array $servers memcached servers to use for caching config info
     */
    static function setupDB($dbhost, $dbuser, $dbpass, $dbname, array $servers)
    {
        global $config;

        $config['db']['database_'.$dbname] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname";
        $config['db']['ini_'.$dbname] = INSTALLDIR.'/classes/status_network.ini';

        foreach (array('status_network', 'status_network_tag', 'unavailable_status_network') as $table) {
            $config['db']['table_'.$table] = $dbname;
        }

        if (class_exists('Memcache')) {
            self::$cache = new Memcache();

            // If we're a parent command-line process we need
            // to be able to close out the connection after
            // forking, so disable persistence.
            //
            // We'll turn it back on again the second time
            // through which will either be in a child process,
            // or a single-process script which is switching
            // configurations.
            $persist = php_sapi_name() != 'cli' || self::$cacheInitialized;
            if (!is_array($servers)) {
                $servers = array($servers);
            }
            foreach($servers as $server) {
                $parts = explode(':', $server);
                $server = $parts[0];
                if (count($parts) > 1) {
                    $port = $parts[1];
                } else {
                    $port = 11211;
                }
                self::$cache->addServer($server, $port, $persist);
            }
            self::$cacheInitialized = true;
        }

        self::$base = $dbname;
    }

    static function cacheKey($k, $v) {
        return 'gnusocial:' . self::$base . ':status_network:'.$k.':'.$v;
    }

    static function memGet($k, $v)
    {
        if (!self::$cache) {
            return self::getKV($k, $v);
        }

        $ck = self::cacheKey($k, $v);

        $sn = self::$cache->get($ck);

        if (empty($sn)) {
            $sn = self::getKV($k, $v);
            if (!empty($sn)) {
                self::$cache->set($ck, clone($sn));
            }
        }

        return $sn;
    }

    function decache()
    {
        if (self::$cache) {
            $keys = array('nickname', 'hostname', 'pathname');
            foreach ($keys as $k) {
                $ck = self::cacheKey($k, $this->$k);
                self::$cache->delete($ck);
            }
        }
    }

    function update($dataObject=false)
    {
        if (is_object($dataObject)) {
            $dataObject->decache(); # might be different keys
        }
        return parent::update($dataObject);
    }

    /**
     * DB_DataObject doesn't allow updating keys (even non-primary)
     */
    function updateKeys(&$orig)
    {
        $this->_connect();
        foreach (array('hostname', 'pathname') as $k) {
            if (strcmp($this->$k, $orig->$k) != 0) {
                $parts[] = $k . ' = ' . $this->_quote($this->$k);
            }
        }
        if (count($parts) == 0) {
            // No changes
            return true;
        }

        $toupdate = implode(', ', $parts);

        $table = common_database_tablename($this->tableName());
        $qry = 'UPDATE ' . $table . ' SET ' . $toupdate .
            ' WHERE nickname = ' . $this->_quote($this->nickname);
        $orig->decache();
        $result = $this->query($qry);
        $this->decache();

        return $result;
    }

    function delete($useWhere=false)
    {
        $this->decache(); # while we still have the values!
        return parent::delete($useWhere);
    }

    /**
     * @param string $servername hostname
     * @param string $wildcard hostname suffix to match wildcard config
     * @return mixed Status_network or null
     */
    static function getFromHostname($servername, $wildcard)
    {
        $sn = null;
        if (0 == strncasecmp(strrev($wildcard), strrev($servername), strlen($wildcard))) {
            // special case for exact match
            if (0 == strcasecmp($servername, $wildcard)) {
                $sn = self::memGet('nickname', '');
            } else {
                $parts = explode('.', $servername);
                $sn = self::memGet('nickname', strtolower($parts[0]));
            }
        } else {
            $sn = self::memGet('hostname', strtolower($servername));

            if (empty($sn)) {
                // Try for a no-www address
                if (0 == strncasecmp($servername, 'www.', 4)) {
                    $sn = self::memGet('hostname', strtolower(substr($servername, 4)));
                }
            }
        }
        return $sn;
    }

    /**
     * @param string $servername hostname
     * @param string $pathname URL base path
     * @param string $wildcard hostname suffix to match wildcard config
     */
    static function setupSite($servername, $pathname, $wildcard)
    {
        global $config;

        $sn = null;

        // XXX I18N, probably not crucial for hostnames
        // XXX This probably needs a tune up
        $sn = self::getFromHostname($servername, $wildcard);

        if (!empty($sn)) {

            // Redirect to the right URL

            if (!empty($sn->hostname) &&
                empty($_SERVER['HTTPS']) &&
                0 != strcasecmp($sn->hostname, $servername)) {
                $sn->redirectTo('http://'.$sn->hostname.$_SERVER['REQUEST_URI']);
            } else if (!empty($_SERVER['HTTPS']) &&
                       0 != strcasecmp($sn->hostname, $servername) &&
                       0 != strcasecmp($sn->nickname.'.'.$wildcard, $servername)) {
                $sn->redirectTo('https://'.$sn->nickname.'.'.$wildcard.$_SERVER['REQUEST_URI']);
            }

            $dbhost = (empty($sn->dbhost)) ? 'localhost' : $sn->dbhost;
            $dbuser = (empty($sn->dbuser)) ? $sn->nickname : $sn->dbuser;
            $dbpass = $sn->dbpass;
            $dbname = (empty($sn->dbname)) ? $sn->nickname : $sn->dbname;

            $config['db']['database'] = "mysqli://$dbuser:$dbpass@$dbhost/$dbname";

            $config['site']['name'] = $sn->sitename;
            $config['site']['nickname'] = $sn->nickname;

            self::$wildcard = $wildcard;

            $config['site']['wildcard'] =& self::$wildcard;

            if (!empty($sn->hostname)) {
                $config['site']['server'] = $sn->hostname;
            }

            if (!empty($sn->theme)) {
                $config['site']['theme'] = $sn->theme;
            }
            if (!empty($sn->logo)) {
                $config['site']['logo'] = $sn->logo;
            }

            return $sn;
        } else {
            return null;
        }
    }

    // Code partially mooked from http://www.richler.de/en/php-redirect/
    // (C) 2006 by Heiko Richler  http://www.richler.de/
    // LGPL

    function redirectTo($destination)
    {
        $old = 'http'.
          (($_SERVER['HTTPS'] == 'on') ? 'S' : '').
          '://'.
          $_SERVER['HTTP_HOST'].
          $_SERVER['REQUEST_URI'].
          $_SERVER['QUERY_STRING'];
        if ($old == $destination) { // this would be a loop!
            // error_log(...) ?
            return false;
        }

        header('HTTP/1.1 301 Moved Permanently');
        header("Location: $destination");

        print "$destination\n";

        exit;
    }

    function getServerName()
    {
        if (!empty($this->hostname)) {
            return $this->hostname;
        } else {
            return $this->nickname . '.' . self::$wildcard;
        }
    }

    /**
     * Return site meta-info tags as an array
     * @return array of strings
     */
    function getTags()
    {
        return Status_network_tag::getTags($this->site_id);
    }

    /**
     * Save a given set of tags
     * @param array tags
     * @fixme only add/remove differentials
     */
    function setTags(array $tags)
    {
        $this->clearTags();
        foreach ($tags as $tag) {
            if (!empty($tag)) {
                $snt = new Status_network_tag();
                $snt->site_id = $this->site_id;
                $snt->tag = $tag;
                $snt->created = common_sql_now();

                $id = $snt->insert();
                if (!$id) {
                    // TRANS: Exception thrown when a tag cannot be saved.
                    throw new Exception(_("Unable to save tag."));
                }
            }
        }

        return true;
    }

    function clearTags()
    {
        $tag = new Status_network_tag();
        $tag->site_id = $this->site_id;

        if ($tag->find()) {
            while($tag->fetch()) {
                $tag->delete();
            }
        }

        $tag->free();
    }

    /**
     * Check if this site record has a particular meta-info tag attached.
     * @param string $tag
     * @return bool
     */
    function hasTag($tag)
    {
        return in_array($tag, $this->getTags());
    }
}