Seditio Source
Root |
./othercms/b2evolution_7.2.3/inc/rest/_restapi.class.php
<?php
/**
 * This file implements REST API class
 *
 * This file is part of the evoCore framework - {@link http://evocore.net/}
 * See also {@link https://github.com/b2evolution/b2evolution}.
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 *
 * @package api
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );

/**
 * REST API
 *
 * @package api
 */
class RestApi
{
   
/**
     * @var string Module name: 'collections', 'users'
     */
   
private $module = NULL;

   
/**
     * @var array Arguments that go from request string,
     *            for example: array( 0 => "collections", 1 => "a", 2 => "posts" )
     */
   
private $args = array();

   
/**
     * @var array Response, This array collects all data that should be printed
     *                      out on screen in JSON format at the end of execution:
     */
   
private $response = array();


   
/**
     * Constructor
     *
     * @param string Request string after "/api/v1/", for example: "collections/a/posts"
     */
   
public function __construct( $request )
    {
       
// Authenticate a user:
       
$this->user_authentication();

       
// Set arguments from request string:
       
$this->args = explode( '/', $request );

        if( isset(
$this->args[0] ) )
        {    
// Initialize module name:
           
$this->module = $this->args[0];
        }
    }


   
/**
     * Log in a user
     *
     * @param string Login
     * @param string Password
     * @return boolean TRUE on success log in, HALT on error
     */
   
private function user_log_in( $entered_login, $entered_password )
    {
        global
$current_User, $failed_logins_lockout, $failed_logins_before_lockout, $UserSettings, $Settings, $Session, $localtimenow;

       
$UserCache = & get_UserCache();

       
// Note: login and password cannot include ' or " or > or <
       
$entered_login = utf8_strtolower( utf8_strip_tags( $entered_login ) );
       
$entered_password = utf8_strip_tags( $entered_password );

        if(
is_email( $entered_login ) )
        {    
// We have an email address instead of login name
            // Get user by email and password:
           
list( $User, $exists_more ) = $UserCache->get_by_emailAndPwd( $entered_login, $entered_password );
        }
        elseif(
is_valid_login( $entered_login ) === true )
        {    
// Make sure that we can load the user:
           
$User = & $UserCache->get_by_login( $entered_login );
        }

        if( empty(
$User ) )
        {    
// Wrong authentication because user is not found by requested login/email and password in DB:
           
if( is_logged_in() )
            {    
// Logout current user:
               
logout();
            }
           
$this->halt( T_('Wrong login/password.'), 'wrong_login_pass', '403' );
           
// Exit here.
       
}

        if(
is_logged_in() )
        {    
// If current user is logged in
           
if( $User->ID == $current_User->ID )
            {    
// The requested user is already logged in, Don't relog in it on each request:
               
return true;
               
// Exit here.
           
}
            else
            {    
// Log out current user because we should log in new user:
               
logout();
            }
        }

       
// Check user login attempts:
       
$login_attempts = $UserSettings->get( 'login_attempts', $User->ID );
       
$login_attempts = empty( $login_attempts ) ? array() : explode( ';', $login_attempts );
        if(
$failed_logins_lockout > 0 && count( $login_attempts ) >= $failed_logins_before_lockout - 1 )
        {    
// User already has a maximum value of the attempts:
           
$first_attempt = explode( '|', $login_attempts[0] );
            if(
$localtimenow - $first_attempt[0] < $failed_logins_lockout )
            {    
// User has used N attempts during X minutes, Display error and Refuse login
               
$this->halt( sprintf( T_('There have been too many failed login attempts. This account is temporarily locked. Try again in %s minutes.'), ceil( $failed_logins_lockout / 60 ) ), 'login_attempt_failed', 403 );
               
// Exit here.
           
}
        }

        if( !
$User->check_password( $entered_password ) )
        {    
// The entered password is not right for requested user
            // Save new login attempt into DB:
           
if( count( $login_attempts ) >= $failed_logins_before_lockout - 1 )
            {
// Unset first attempt to clear a space for new attempt
               
unset( $login_attempts[0] );
            }
           
$login_attempts[] = $localtimenow.'|'.( array_key_exists( 'REMOTE_ADDR', $_SERVER ) ? $_SERVER['REMOTE_ADDR'] : '' );
           
$UserSettings->set( 'login_attempts', implode( ';', $login_attempts ), $User->ID );
           
$UserSettings->dbupdate();

           
$this->halt( T_('Wrong login/password.'), 'wrong_login_pass', '403' );
           
// Exit here.
       
}

        if(
$User->check_status( 'is_closed' ) )
        {    
// User account was closed, don't log in it:
           
$this->halt( T_('This account is closed. You cannot log in.'), 'closed_account', '403' );
           
// Exit here.
       
}
        elseif(
$Settings->get( 'system_lock' ) && ! $User->check_perm( 'users', 'edit' ) )
        {
// System is locked for maintenance and current user has no permission to log in this mode
           
$this->halt( T_('You cannot log in at this time because the system is under maintenance. Please try again in a few moments.'), 'system_maintenance', '503' );
           
// Exit here.
       
}

       
// All checks are good, so we can log in the requested user:
       
$current_User = $User;
       
$Session->set_User( $current_User );

        if( ! empty(
$login_attempts ) )
        {    
// Clear the attempts list:
           
$UserSettings->delete( 'login_attempts', $current_User->ID );
           
$UserSettings->dbupdate();
        }

        return
true;
    }


   
/**
     * Authenticate a user
     */
   
private function user_authentication()
    {
        global
$current_User;

        if( isset(
$_SERVER, $_SERVER['PHP_AUTH_USER'] ) &&
            ( !
is_logged_in() || $current_User->get( 'login' ) != $_SERVER['PHP_AUTH_USER'] ) )
        {    
// Do basic HTTP authentication when user login is provided AND
            // user is not logged in yet OR the provided login is defferent than login of the current User:
           
$this->user_log_in( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] );
        }
    }


   
/**
     * Execute request
     *
     * @param boolean TRUE on success result
     */
   
public function execute()
    {
        if( empty(
$this->module ) )
        {    
// Wrong request because module is not defined:
           
$this->halt( 'Module is not defined' );
           
// Exit here.
       
}

        if( !
method_exists( $this, 'module_'.$this->module ) )
        {    
// Unknown module:
           
$this->halt( 'Unknown module "'.$this->module.'"', 'unknown_module' );
           
// Exit here.
       
}

       
// Call module to prepare current request:
       
$this->{'module_'.$this->module}();

       
// Print out response in JSON format:
       
$this->display_response();
    }


   
/**
     * Halt execution
     *
     * @param string Error text
     * @param integer Status code: 'wrong_route', 'unknown_module'
     * @param integer Status number: 404, 403
     */
   
private function halt( $error_text, $status_code = 'wrong_route', $status_number = 404 )
    {
       
// Set status code number:
       
header_http_response( $status_number );

       
// Add info about error:
       
$this->add_response( 'code', $status_code );
       
$this->add_response( 'message', $error_text );
       
$this->add_response( 'data', array( 'status' => $status_number ) );

       
// Print out response in JSON format:
       
$this->display_response();

       
// Halt execution here:
       
exit();
    }


   
/**
     * Halt execution with text from $Messages
     *
     * @param integer Status code: 'wrong_route', 'unknown_module'
     * @param integer Status number: 404, 403
     * @param string Message types, separated by comma: 'success', 'warning', 'error', 'note'
     */
   
private function halt_with_Messages( $status_code = 'no_access', $status_number = 403, $message_types = 'error,warning' )
    {
        global
$Messages;

       
$message_types = explode( ',', $message_types );

       
$halt_messages = array();
       
$Messages->close_group(); // Make sure any open message group are closed
       
foreach( $Messages->messages_text as $m => $message_text )
        {
            if(
in_array( $Messages->messages_type[ $m ], $message_types ) )
            {    
// Get this message for halt because of message type:
               
$halt_messages[] = $message_text;
            }
        }

       
$halt_messages = empty( $halt_messages ) ? $status_code : implode( ' ', $halt_messages );

       
// Halt execution:
       
$this->halt( $halt_messages, $status_code, $status_number );
    }


   
/**
     * Add new element in response array
     *
     * @param string Key or Value ( if second param is NULL ), Use NULL to merge the array from second param with existing response array
     * @param mixed Value
     * @param string Type of new added item: 'raw', 'integer', 'array'
     */
   
private function add_response( $key, $value = NULL, $type = 'raw' )
    {
        if(
$value === NULL )
        {    
// Use auto key:
           
$this->response[] = $key;
        }
        elseif(
$key === NULL && is_array( $value ) )
        {    
// Merge new data array to response array:
           
$this->response = array_merge( $this->response, $value );
        }
        else
        {    
// Use defined key:
           
switch( $type )
            {
                case
'array':
                    if( ! isset(
$this->response[ $key ] ) || ! is_array( $this->response[ $key ] ) )
                    {    
// Initialize array:
                       
$this->response[ $key ] = array();
                    }
                   
// Add new item to array:
                   
$this->response[ $key ][] = $value;
                    break;

                case
'integer':
                   
$this->response[ $key ] = intval( $value );
                    break;

                default:
                   
// raw (does nothing)
                   
$this->response[ $key ] = $value;
            }
        }
    }


   
/**
     * Print out the response array in JSON format:
     */
   
private function display_response()
    {
       
// Send the predefined cookies:
       
evo_sendcookies();

       
// Set JSON content type:
       
headers_content_mightcache( 'application/json' );

       
// Convert array to JSON format and print out:
       
echo json_encode( $this->response );
    }


   
/**** MODULE COLLECTIONS ---- START ****/


    /**
     * Call module to prepare request for collections
     */
   
private function module_collections()
    {
        global
$Collection, $Blog;

        switch(
$_SERVER['REQUEST_METHOD'] )
        {
            case
'GET':
               
// List of valid resources
               
$valid_resources = array( '', 'view', 'items', 'posts', 'search', 'assignees', 'linked' );
                break;

            case
'PUT':
               
// List of valid resources
               
$valid_resources = array( 'favorite', 'items_flag' );
                break;

            default:
               
$this->halt( 'Request method not supported for the requested resource', 'wrong_request', 405 );
               
// Exit here
       
}

        if( ! empty(
$this->args[1] ) )
        {
           
$coll_controller = ! empty( $this->args[2] ) ? $this->args[2] : 'view';
        }
        else
        {
           
$coll_controller = '';
        }

        if( ! empty(
$coll_controller ) )
        {    
// Initialize data for request of the selected collection:

            // Collection urlname:
           
$coll_urlname = $this->args[1];

           
$BlogCache = & get_BlogCache();
            if( (
$Collection = $Blog = $BlogCache->get_by_urlname( $coll_urlname, false ) ) === false )
            {    
// Collection is not detected in DB by requested url name:
               
$this->halt( 'No collection found in DB by requested url name "'.$coll_urlname.'"', 'unknown_collection', 404 );
               
// Exit here.
           
}

           
// Check access to the requested collection:
           
$allow_access = $Blog->get_setting( 'allow_access' );
            if(
$allow_access != 'public' )
            {    
// If the collection is not public:
               
if( ! is_logged_in() )
                {    
// Only logged in users have an access to the collection:
                   
$this->halt( T_('You need to log in before you can access this section.'), 'access_requires_login', 403 );
                   
// Exit here.
               
}
                elseif(
$allow_access == 'members' )
                {    
// Check if current user is member of the collection:
                   
if( ! check_user_perm( 'blog_ismember', 'view', false, $Blog->ID ) )
                    {    
// Current user cannot access to the collection:
                       
$this->halt( T_('You are not a member of this section, therefore you are not allowed to access it.'), 'access_denied', 403 );
                       
// Exit here.
                   
}
                }
            }
        }

       
// Try to get an object ID and action for example request "<baseurl>/api/v1/collections/<collname>/items/<id>/action":
       
$object_ID = empty( $this->args[3] ) ? NULL : intval( $this->args[3] );
       
$object_action = empty( $this->args[4] ) ? NULL : $this->args[4];
       
$coll_controller_params = array();

        if(
$object_ID && $object_action )
        {    
// Set controller name for single object and action:
           
$coll_controller = $coll_controller.'_'.$object_action;
           
$coll_controller_params[] = $object_ID;
        }

        if( !
method_exists( $this, 'controller_coll_'.$coll_controller ) )
        {    
// Unknown controller:
           
$this->halt( 'Unknown collection controller "'.$coll_controller.'"', 'unknown_controller' );
           
// Exit here.
       
}

        if( !
in_array( $coll_controller, $valid_resources, true ) )
        {
// Invalid request method:
           
$this->halt( 'Request method not supported for the requested resource', 'wrong_request', 405 );
           
// Exit here.
       
}

       
// Call collection controller to prepare current request:
       
call_user_func_array( array( $this, 'controller_coll_'.$coll_controller ), $coll_controller_params );
    }


   
/**
     * Default controller/handler
     * erhsatingin > function name is a bit wonky, perhaps there's a better way to go around this.
     */
   
private function controller_coll_()
    {
        global
$DB, $current_User;

       
$api_page = param( 'page', 'integer', 1 );
       
$api_per_page = param( 'per_page', 'integer', 10 );
       
$api_q = param( 'q', 'string', '' );
       
$api_fields = param( 'fields', 'string', 'shortname' ); // 'id', 'shortname'
       
$api_restrict_to_available_fileroots = param( 'restrict_to_available_fileroots', 'integer', 0 ); // 1 - Load only collections with available file roots for current user
       
$api_list_in_frontoffice = param( 'list_in_frontoffice', 'string', 'public' ); // 'public' - Load only collections which can be viewed for current user

       
$BlogCache = & get_BlogCache();

        if(
$api_list_in_frontoffice == 'public' )
        {    
// SQL to get ONLY public collections:
           
$SQL = $BlogCache->get_public_colls_SQL();
           
$count_SQL = $BlogCache->get_public_colls_SQL();
           
$count_SQL->SELECT( 'COUNT( blog_ID )' );
        }
        else
        {    
// SQL to get ALL collections that can be seen by currently logged in User:
           
$SQL = $BlogCache->get_available_colls_SQL();
           
$count_SQL = $BlogCache->get_available_colls_SQL();
           
$count_SQL->SELECT( 'COUNT( blog_ID )' );
        }

        if( ! empty(
$api_q ) )
        {    
// Search collections by keyword:
           
$search_sql_where = array();
           
$search_fields = empty( $api_fields ) ? array( 'shortname' ) : explode( ',', $api_fields );
            foreach(
$search_fields as $search_field )
            {
                switch(
strtolower( $search_field ) )
                {
                    case
'id':
                       
$search_sql_where[] = 'blog_ID = '.$DB->quote( $api_q );
                        break;

                    case
'shortname':
                       
$search_sql_where[] = 'blog_shortname LIKE '.$DB->quote( '%'.$api_q.'%' );
                        break;
                }
            }

           
$search_sql_where = implode( ' OR ', $search_sql_where );
           
$SQL->WHERE_and( $search_sql_where );
           
$count_SQL->WHERE_and( $search_sql_where );
        }

       
$collections = array();
        if(
$api_restrict_to_available_fileroots &&
            (
              !
is_logged_in() ||
              !
check_user_perm( 'admin', 'restricted' ) ||
              !
check_user_perm( 'files', 'view' )
            ) )
        {    
// Anonymous user has no access to file roots AND also if current use has no access to back-office or to file manager:
           
$result_count = 0;
        }
        else
        {
            if(
$api_restrict_to_available_fileroots )
            {    
// Restrict collections by available file roots for current user:

                // SQL analog for check_user_perm( 'blogs', 'view' ) || check_user_perm( 'files', 'edit' ):
               
$current_User->get_Group();
               
$check_perm_blogs_view_files_edit_SQL = new SQL();
               
$check_perm_blogs_view_files_edit_SQL->SELECT( 'grp_ID' );
               
$check_perm_blogs_view_files_edit_SQL->FROM( 'T_groups' );
               
$check_perm_blogs_view_files_edit_SQL->FROM_add( 'LEFT JOIN T_groups__groupsettings ON grp_ID = gset_grp_ID AND gset_name = "perm_files"' );
               
$check_perm_blogs_view_files_edit_SQL->WHERE( 'grp_ID = '.$current_User->Group->ID );
               
$check_perm_blogs_view_files_edit_SQL->WHERE_and( 'grp_perm_blogs IN ( "viewall", "editall" ) OR gset_value IS NULL OR gset_value IN ( "all", "edit" )' );
               
$restrict_available_fileroots_sql = '( '.$check_perm_blogs_view_files_edit_SQL->get().' )';

               
// SQL analog for check_user_perm( 'blog_media_browse', 'view', false, $Blog ):
               
$check_perm_blog_media_browse_user_SQL = new SQL();
               
$check_perm_blog_media_browse_user_SQL->SELECT( 'bloguser_blog_ID' );
               
$check_perm_blog_media_browse_user_SQL->FROM( 'T_coll_user_perms' );
               
$check_perm_blog_media_browse_user_SQL->WHERE( 'bloguser_user_ID = '.$current_User->ID );
               
$check_perm_blog_media_browse_user_SQL->WHERE_and( 'bloguser_perm_media_browse <> 0' );
               
$check_perm_blog_media_browse_group_SQL = new SQL();
               
$check_perm_blog_media_browse_group_SQL->SELECT( 'bloggroup_blog_ID' );
               
$check_perm_blog_media_browse_group_SQL->FROM( 'T_coll_group_perms' );
               
$check_perm_blog_media_browse_group_SQL->WHERE( '( bloggroup_group_ID = '.$current_User->Group->ID.'
                    OR bloggroup_group_ID IN ( SELECT sug_grp_ID FROM T_users__secondary_user_groups WHERE sug_user_ID = '
.$current_User->ID.' ) )' );
               
$check_perm_blog_media_browse_group_SQL->WHERE_and( 'bloggroup_perm_media_browse <> 0' );
               
$restrict_available_fileroots_sql .= ' OR blog_owner_user_ID = '.$current_User->ID
                   
.' OR ( blog_advanced_perms <> 0 AND ( '
                   
.'        blog_ID IN ( '.$check_perm_blog_media_browse_user_SQL->get().' ) OR '
                   
.'        blog_ID IN ( '.$check_perm_blog_media_browse_group_SQL->get().' )'
                   
.'      )'
                   
.'    )';

               
$SQL->WHERE_and( $restrict_available_fileroots_sql );
               
$count_SQL->WHERE_and( $restrict_available_fileroots_sql );
            }

           
$result_count = intval( $DB->get_var( $count_SQL ) );
        }

       
// Prepare pagination:
       
if( $api_per_page > 0 && $result_count > $api_per_page )
        {    
// We will have multiple search result pages:
           
if( $api_page < 1 )
            {    
// Limit by min page:
               
$api_page = 1;
            }
           
$total_pages = ceil( $result_count / $api_per_page );
            if(
$api_page > $total_pages )
            {    
// Limit by max page:
               
$api_page = $total_pages;
            }
        }
        else
        {    
// Only one page of results:
           
$api_page = 1;
           
$total_pages = 1;
        }

       
$BlogCache = & get_BlogCache();
       
$BlogCache->clear();

        if(
$result_count > 0 )
        {    
// Select collections only from current page:
           
if( $api_per_page > 0 )
            {    
// Limit results by page size only when this is no unlimitted request:
               
$SQL->LIMIT( ( ( $api_page - 1 ) * $api_per_page ).', '.$api_per_page );
            }
           
$BlogCache->load_by_sql( $SQL );
        }

       
$this->add_response( 'found', $result_count, 'integer' );
       
$this->add_response( 'page', $api_page, 'integer' );
       
$this->add_response( 'page_size', $api_per_page, 'integer' );
       
$this->add_response( 'pages_total', $total_pages, 'integer' );

        foreach(
$BlogCache->cache as $Blog )
        {    
// Add each collection row in the response array:
           
$this->add_response( 'colls', array(
                   
'id'        => intval( $Blog->ID ),
                   
'urlname'   => $Blog->get( 'urlname' ),
                   
'kind'      => $Blog->get( 'type' ),
                   
'shortname' => $Blog->get( 'shortname' ),
                   
'name'      => $Blog->get( 'name' ),
                   
'tagline'   => $Blog->get( 'tagline' ),
                   
'desc'      => $Blog->get( 'longdesc' ),
                ),
'array' );
        }
    }


   
/**
     * Call collection controller to prepare request for linked collections
     */
   
private function controller_coll_linked()
    {
        global
$Collection, $Blog;

       
// Get linked collections:
       
$linked_colls = $Blog->get_locales( 'coll' );
       
// Add current collection on top:
       
array_unshift( $linked_colls, $Blog->ID );

       
// Load all collections in cache by single SQL query:
       
$BlogCache = & get_BlogCache();
       
$BlogCache->load_list( $linked_colls );

        foreach(
$linked_colls as $linked_locale => $linked_coll_ID )
        {    
// Add each collection row in the response array:
           
if( ! ( $linked_Blog = & $BlogCache->get_by_ID( $linked_coll_ID, false, false ) ) )
            {    
// Skip wrong linked collection:
               
continue;
            }
           
$this->add_response( 'colls', array(
                   
'id'        => intval( $linked_Blog->ID ),
                   
'urlname'   => $linked_Blog->get( 'urlname' ),
                   
'kind'      => $linked_Blog->get( 'type' ),
                   
'shortname' => $linked_Blog->get( 'shortname' ),
                   
'name'      => $linked_Blog->get( 'name' ).' ('.( $linked_locale === 0 ? $linked_Blog->get( 'locale' ) : $linked_locale ).')',
                   
'tagline'   => $linked_Blog->get( 'tagline' ),
                   
'desc'      => $linked_Blog->get( 'longdesc' ),
                ),
'array' );
        }
    }


   
/**
     * Call collection controller to view collection information
     */
   
private function controller_coll_view()
    {
       
$coll_urlname = empty( $this->args[1] ) ? 0 : $this->args[1];

       
$BlogCache = & get_BlogCache();
       
$Blog = & $BlogCache->get_by_urlname( $coll_urlname, false );

       
$collection_data = array(
           
'id'        => intval( $Blog->ID ),
           
'urlname'   => $Blog->get( 'urlname' ),
           
'kind'      => $Blog->get( 'type' ),
           
'shortname' => $Blog->get( 'shortname' ),
           
'name'      => $Blog->get( 'name' ),
           
'tagline'   => $Blog->get( 'tagline' ),
           
'desc'      => $Blog->get( 'longdesc' ) );

       
$this->add_response( NULL, $collection_data );
    }


   
/**
     * Call collection controller to prepare request for items with ANY types
     *
     * @param array Force filters of request
     */
   
private function controller_coll_items( $force_filters = array() )
    {
        global
$Collection, $Blog;

       
// Get param to limit number posts per page:
       
$api_per_page = param( 'per_page', 'integer', 10 );

       
// Get param to select current page:
        // (NOTE: if this param is not set then param 'paged' is used as filter of ItemList)
       
$page = param( 'page', 'integer', NULL );

       
// Get param to know what post fields should be sent in response:
       
$api_details = param( 'details', 'string', NULL );

       
// Try to get a post ID for request "<baseurl>/api/v1/collections/<collname>/items/<id>":
       
$post_ID = empty( $this->args[3] ) ? 0 : $this->args[3];

       
// Get params for full content:
       
$content_params = param( 'content_params', 'array', array() );
       
$content_params =  array_merge( array(
           
'before_content_teaser'    => '',
           
'after_content_teaser'     => '',
           
'before_content_extension' => '',
           
'after_content_extension'  => '',
           
'image_position_teaser'    => 'teaser,teaserperm,teaserlink',
           
'image_position_aftermore' => 'aftermore',
           
'before_images'            => '',
           
'after_images'             => '',
           
'before_image'             => '<figure class="evo_image_block">',
           
'before_image_legend'      => '<figcaption class="evo_image_legend">',
           
'after_image_legend'       => '</figcaption>',
           
'after_image'              => '</figure>',
           
'image_class'              => '',
           
'image_size'               => 'original',
           
'image_limit'              =>  1000,
           
'image_link_to'            => 'original', // Can be 'original', 'single' or empty
           
'before_gallery'           => '<div class="evo_post_gallery">',
           
'after_gallery'            => '</div>',
           
'gallery_table_start'      => '',
           
'gallery_table_end'        => '',
           
'gallery_row_start'        => '',
           
'gallery_row_end'          => '',
           
'gallery_cell_start'       => '<div class="evo_post_gallery__image">',
           
'gallery_cell_end'         => '</div>',
           
'gallery_image_size'       => 'crop-80x80',
           
'gallery_image_limit'      => 1000,
           
'gallery_image_link_to' => 'original', // Can be 'original', 'single' or empty
           
'gallery_colls'            => 5,
           
'gallery_order'            => '', // Can be 'ASC', 'DESC', 'RAND' or empty
       
), $content_params );

       
$ItemList2 = new ItemList2( $Blog, $Blog->get_timestamp_min(), $Blog->get_timestamp_max(), $api_per_page, 'ItemCache', '' );

        if(
$post_ID )
        {    
// Get only one requested post:
           
if( ctype_digit( ( string ) $post_ID ) )
            {    
// Get by post ID:
               
$ItemList2->set_filters( array( 'post_ID' => $post_ID ), true, true );
            }
            else
            {    
// Get by url slug:
               
$ItemList2->set_filters( array( 'post_title' => $post_ID ), true, true );
            }
        }
        else
        {    
// Load all available params from request to filter the posts list:
           
$ItemList2->load_from_Request( false );
        }

        if(
$ItemList2->filters['types'] == $ItemList2->default_filters['types'] )
        {    
// Allow all post types by default for this request:
           
$ItemList2->set_filters( array( 'itemtype_usage' => NULL ), true, true );
        }

        if(
$page !== NULL )
        {    
// Set page from request:
           
$ItemList2->set_filters( array( 'page' => $page ), true, true );
        }

        if( ! empty(
$force_filters ) )
        {    
// Force filters:
           
$ItemList2->set_filters( $force_filters, true, true );
        }

       
// Run the items list query:
       
$ItemList2->query();

        if( !
$post_ID )
        {    
// Add data of posts list to response:
           
$this->add_response( 'found', $ItemList2->total_rows, 'integer' );
           
$this->add_response( 'page', $ItemList2->page, 'integer' );
           
$this->add_response( 'page_size', $ItemList2->limit, 'integer' );
           
$this->add_response( 'pages_total', $ItemList2->total_pages, 'integer' );
        }

        if(
$api_details == '*' || ( $post_ID && empty( $api_details ) ) )
        {    
// Use all possible fields for single post request or if it is defined to current request:
           
$api_details = array(
                   
'id',
                   
'datestart',
                   
'urltitle',
                   
'type',
                   
'title',
                   
'content',
                   
'excerpt',
                   
'teaser',
                   
'URL',
                   
'attachments',
                );
        }
        elseif( empty(
$api_details ) )
        {    
// For posts list get only ID and title by default:
           
$api_details = array( 'id', 'title' );
        }
        else
        {    
// Use custom fields from request:
           
$api_details = explode( ',', $api_details );
        }

       
// Add each post row in the response array:
       
while( $Item = & $ItemList2->get_next() )
        {
           
// Initialize data for each item:
           
$item_data = array();
            foreach(
$api_details as $api_details_field )
            {
                switch(
$api_details_field )
                {
                    case
'id':
                       
$item_data['id'] = intval( $Item->ID );
                        break;
                    case
'datestart':
                       
$item_data['datestart'] = $Item->get( 'datestart' );
                        break;
                    case
'urltitle':
                       
$item_data['urltitle'] = $Item->get( 'urltitle' );
                        break;
                    case
'type':
                       
$item_data['type'] = $Item->get_type_setting( 'name' );
                        break;
                    case
'title':
                       
$item_data['title'] = $Item->get( 'title' );
                        break;
                    case
'content':
                       
$item_data['content'] =
                           
$Item->get_images( array_merge( $content_params, array(
                                   
'before' => $content_params['before_images'],
                                   
'after'  => $content_params['after_images'],
                                   
'limit'  => $content_params['image_limit'],
                                   
'restrict_to_image_position' => $content_params['image_position_teaser'],
                                ) ) ).
                           
$Item->get_content_teaser( '#', '#', 'htmlbody', array_merge( $content_params, array(
                                   
'before' => $content_params['before_content_teaser'],
                                   
'after'  => $content_params['after_content_teaser'],
                                ) ) ).
                           
$Item->get_images( array_merge( $content_params, array(
                                   
'before' => $content_params['before_images'],
                                   
'after'  => $content_params['after_images'],
                                   
'limit'  => $content_params['image_limit'],
                                   
'restrict_to_image_position' => $content_params['image_position_aftermore'],
                                ) ) ).
                           
$Item->get_content_extension( '#', true, 'htmlbody', array_merge( $content_params, array(
                                   
'before' => $content_params['before_content_extension'],
                                   
'after'  => $content_params['after_content_extension'],
                                ) ) );
                        break;
                    case
'excerpt':
                       
$item_data['excerpt'] = $Item->get( 'excerpt' );
                        break;
                    case
'teaser':
                       
$item_data['teaser'] = $Item->get_content_teaser();
                        break;
                    case
'URL':
                       
$item_data['URL'] = $Item->get_permanent_url( '', '', '&' );
                        break;
                    case
'attachments':
                       
// Get all(1000) item attachemnts:
                       
$attachments = array();
                       
$LinkOwner = new LinkItem( $Item );
                        if(
$LinkList = $LinkOwner->get_attachment_LinkList( 1000 ) )
                        {
                            while(
$Link = & $LinkList->get_next() )
                            {
                                if( ! (
$File = & $Link->get_File() ) )
                                {    
// No File object
                                   
global $Debuglog;
                                   
$Debuglog->add( sprintf( 'Link ID#%d of item #%d does not have a file object!', $Link->ID, $Item->ID ), array( 'error', 'files' ) );
                                    continue;
                                }

                                if( !
$File->exists() )
                                {    
// File doesn't exist
                                   
global $Debuglog;
                                   
$Debuglog->add( sprintf( 'File linked to item #%d does not exist (%s)!', $Item->ID, $File->get_full_path() ), array( 'error', 'files' ) );
                                    continue;
                                }

                               
$attachments[] = array(
                                       
'link_ID'  => intval( $Link->ID ),
                                       
'file_ID'  => intval( $File->ID ),
                                       
'type'     => strval( $File->is_dir() ? 'dir' : $File->type ),
                                       
'position' => $Link->get( 'position' ),
                                       
'name'     => $File->get_name(),
                                       
'url'      => $File->get_url(),
                                       
'title'    => strval( $File->get( 'title' ) ),
                                       
'alt'      => strval( $File->get( 'alt' ) ),
                                       
'desc'     => strval( $File->get( 'desc' ) ),
                                    );
                            }
                        }
                       
$item_data['attachments'] = $attachments;
                        break;
                }
            }

            if(
$post_ID )
            {    
// If only one post is requested then response should as one level array with post fields:
               
$this->add_response( NULL, $item_data );
            }
            else
            {    
// Add data of each post in separate array of response:
               
$this->add_response( 'items', $item_data, 'array' );
            }
        }

        if( empty(
$item_data ) )
        {    
// No posts detected:
           
if( $post_ID )
            {    
// Wrong post request:
               
$this->halt( 'Invalid post ID', 'post_invalid_id', 404 );
               
// Exit here.
           
}
            else
            {    
// No posts found:
               
$this->halt( 'No posts found for requested collection', 'no_posts', 404 );
               
// Exit here.
           
}
        }
    }


   
/**
     * Call collection controller to prepare request for items ONLY with item type "post"
     */
   
private function controller_coll_posts()
    {
       
$this->controller_coll_items( array(
               
'itemtype_usage' => 'post', // Keep content post types, Exclude pages, intros, sidebar links and ads
           
) );
    }


   
/**
     * Call collection controller to search the chapters, posts, comments and tags
     */
   
private function controller_coll_search()
    {
        global
$Collection, $Blog, $Session;

       
// Get additional params:
       
$api_page = param( 'page', 'integer', 1 );
       
$api_per_page = param( 'per_page', 'integer', 10 );
       
$api_exclude_posts = param( 'exclude_posts', 'string', '' );
       
// What types search: 'all', 'item', 'comment', 'category', 'tag'
        // Use separator comma to use several kinds:
       
$api_kind = param( 'kind', 'string', 'all' );

       
// Load the search functions:
       
load_funcs( 'collections/_search.funcs.php' );

       
// Set a search string:
       
$search_keywords = empty( $this->args[3] ) ? '' : urldecode( $this->args[3] );

       
// Try to load existing search results from Session:
       
$search_params = $Session->get( 'search_params' );
       
$search_result = $Session->get( 'search_result' );
       
$search_result_loaded = false;
        if( empty(
$search_params )
            || (
$search_params['search_keywords'] != $search_keywords ) // We had saved search results but for a different search string
           
|| ( $search_params['search_blog'] != $Blog->ID ) // We had saved search results but for a different collection
           
|| ( isset( $search_params['exclude_posts'] ) && $search_params['exclude_posts'] != $api_exclude_posts ) // We had saved search results but for a different posts excluding
           
|| ( $search_result === NULL ) )
        {    
// We need to perform a new search:
           
$search_params = array(
               
'search_keywords' => $search_keywords,
               
'search_blog'     => $Blog->ID,
               
'exclude_posts'   => $api_exclude_posts,
            );

           
// Perform new search:
           
$search_result = perform_scored_search( $search_keywords, $api_kind, $api_exclude_posts );

           
// Save results into session:
           
$Session->set( 'search_params', $search_params );
           
$Session->set( 'search_result', $search_result );
           
$search_result_loaded = true;
        }

       
$search_result = $Session->get( 'search_result' );
        if( empty(
$search_result ) )
        {    
// Nothing found:
           
$this->add_response( 'found', 0, 'integer' );
           
$this->add_response( 'page', $api_page, 'integer' );
           
$this->add_response( 'page_size', $api_per_page, 'integer' );
           
$this->add_response( 'pages_total', 0, 'integer' );
           
$this->add_response( 'results', array() );
        }
        else
        {
           
// Prepare pagination:
           
$result_count = count( $search_result );
           
$result_per_page = $api_per_page;
            if(
$result_count > $result_per_page )
            {    
// We will have multiple search result pages:
               
$current_page = $api_page;
                if(
$current_page < 1 )
                {
                   
$current_page = 1;
                }
               
$total_pages = ceil( $result_count / $result_per_page );
                if(
$api_page > $total_pages )
                {
                   
$current_page = $total_pages;
                }
            }
            else
            {    
// Only one page of results:
               
$current_page = 1;
               
$total_pages = 1;
            }

           
// Set current page indexes:
           
$from = ( ( $current_page -1 ) * $result_per_page );
           
$to = ( $current_page < $total_pages ) ? ( $from + $result_per_page ) : ( $result_count );

           
// Init caches
           
$ItemCache = & get_ItemCache();
           
$CommentCache = & get_CommentCache();
           
$ChapterCache = & get_ChapterCache();

            if( !
$search_result_loaded )
            {    
// Search result objects are not loaded into memory yet, load them:
                // Group required object ids by type:
               
$required_ids = array();
                for(
$index = $from; $index < $to; $index++ )
                {
                   
$row = $search_result[ $index ];
                    if( isset(
$required_ids[ $row['type'] ] ) )
                    {
                       
$required_ids[ $row['type'] ][] = $row['ID'];
                    }
                    else
                    {
                       
$required_ids[ $row['type'] ] = array( $row['ID'] );
                    }
                }

               
// Load each required object into the corresponding cache:
               
foreach( $required_ids as $type => $object_ids )
                {
                    switch(
$type )
                    {
                        case
'item':
                           
$ItemCache->load_list( $object_ids );
                            break;

                        case
'comment':
                           
$CommentCache->load_list( $object_ids );
                            break;

                        case
'category':
                           
$ChapterCache->load_list( $object_ids );
                            break;

                       
// TODO: we'll probably load "tag" objects once we support tag-synonyms.

                       
default: // Not handled search result type
                           
break;
                    }
                }
            }

           
$this->add_response( 'found', $result_count, 'integer' );
           
$this->add_response( 'page', $current_page, 'integer' );
           
$this->add_response( 'page_size', $result_per_page, 'integer' );
           
$this->add_response( 'pages_total', $total_pages, 'integer' );

           
// Get results for current page:
           
for( $index = $from; $index < $to; $index++ )
            {
               
$row = $search_result[ $index ];

               
$result_data = array(
                       
'kind' => $row['type'],
                       
'id'   => intval( $row['ID'] ),
                    );

                switch(
$row['type'] )
                {
                    case
'item':
                       
// Prepare to display an Item:

                       
$Item = $ItemCache->get_by_ID( $row['ID'], false );

                        if( empty(
$Item ) )
                        {
// This Item was deleted, since the search process was executed
                           
continue 2; // skip from switch and skip to the next item in loop
                       
}

                       
$result_data['title'] = $Item->get_title( array( 'link_type' => 'none' ) );
                       
$result_data['desc'] = $Item->get_excerpt();
                       
$result_data['permalink'] = $Item->get_permanent_url( '', '', '&' );
                        break;

                    case
'comment':
                       
// Prepare to display a Comment:

                       
$Comment = $CommentCache->get_by_ID( $row['ID'], false );

                        if( empty(
$Comment ) || ( $Comment->status == 'trash' ) )
                        {
// This Comment was deleted, since the search process was executed
                           
continue 2; // skip from switch and skip to the next item in loop
                       
}

                       
$comment_Item = & $Comment->get_Item();
                       
$result_data['title'] = $comment_Item->get_title( array( 'link_type' => 'none' ) );
                       
$result_data['desc'] = excerpt( $Comment->content );
                       
$result_data['permalink'] = $Comment->get_permanent_url( '&' );
                        break;

                    case
'category':
                       
// Prepare to display a Category:

                       
$Chapter = $ChapterCache->get_by_ID( $row['ID'], false );

                        if( empty(
$Chapter ) )
                        {
// This Chapter was deleted, since the search process was executed
                           
continue 2; // skip from switch and skip to the next item in loop
                       
}

                       
$result_data['title'] = $Chapter->get_name();
                       
$result_data['desc'] = excerpt( $Chapter->get( 'description' ) );
                       
$result_data['permalink'] = $Chapter->get_permanent_url( NULL, NULL, 1, NULL, '&' );
                        break;

                    case
'tag':
                       
// Prepare to display a Tag:

                       
list( $tag_name, $post_count ) = explode( ',', $row['name'] );

                       
$result_data['title'] = $tag_name;
                       
$result_data['desc'] = sprintf( T_('%d posts are tagged with \'%s\''), $post_count, $tag_name );
                       
$result_data['permalink'] = url_add_param( $Blog->gen_blogurl(), 'tag='.$tag_name, '&' );
                        break;

                    default:
                       
// Other type of result is not implemented

                        // TODO: maybe find collections (especially in case of aggregation)? users? files?

                       
continue 2;
                }

               
// Add data of the searched thing to response:
               
$this->add_response( 'results', $result_data, 'array' );
            }
        }
    }


   
/**
     * Call collection controller to prepare request for possible assignees
     *
     * Request scheme: "<baseurl>/api/v1/collections/<collname>/assignees?q=<search login string>"
     *
     * @param integer Item ID
     */
   
private function controller_coll_assignees()
    {
        global
$Collection, $Blog, $DB;

        if( !
check_user_perm( 'blog_can_be_assignee', 'edit', false, $Blog->ID ) )
        {    
// Check permission: Current user must has a permission to be assignee of the collection:
           
$this->halt( 'You are not allowed to view assigness of the collection "'.$Blog->get( 'name' ).'".', 'no_access', 403 );
           
// Exit here.
       
}

        if( !
$Blog->get_setting( 'use_workflow' ) )
        {    
// If workflow is not enabled for the collection:
           
$this->halt( 'The collection "'.$Blog->get( 'name' ).'" is not used for workflow.', 'no_access', 403 );
           
// Exit here.
       
}

       
$api_q = trim( urldecode( param( 'q', 'string', '' ) ) );

       
/**
         * sam2kb> The code below decodes percent-encoded unicode string produced by Javascript "escape"
         * function in format %uxxxx where xxxx is a Unicode value represented as four hexadecimal digits.
         * Example string "MAMA" (cyrillic letters) encoded with "escape": %u041C%u0410%u041C%u0410
         * Same word encoded with "encodeURI": %D0%9C%D0%90%D0%9C%D0%90
         *
         * jQuery hintbox plugin uses "escape" function to encode URIs
         *
         * More info here: http://en.wikipedia.org/wiki/Percent-encoding#Non-standard_implementations
         */
       
if( preg_match( '~%u[0-9a-f]{3,4}~i', $api_q ) )
        {    
// Decode UTF-8 string:
           
$api_q = preg_replace( '~%u([0-9a-f]{3,4})~i', '&#x\\1;', $api_q );
           
$api_q = html_entity_decode( $api_q, ENT_COMPAT, 'UTF-8' );
        }

        if( empty(
$api_q ) )
        {    
// Don't allow empty request:
           
$this->halt( 'Please enter at least one char to find assignees', 'no_access', 403 );
           
// Exit here.
       
}

        if(
$Blog->get( 'advanced_perms' ) )
        {    
// Load group and user permissions ONLY if collection advanced permissions are enabled:

            // Get users which can be assignees of the collection:
           
$user_perms_SQL = new SQL();
           
$user_perms_SQL->SELECT( 'user_login' );
           
$user_perms_SQL->FROM( 'T_users' );
           
$user_perms_SQL->FROM_add( 'INNER JOIN T_coll_user_perms ON user_ID = bloguser_user_ID' );
           
$user_perms_SQL->WHERE( 'bloguser_blog_ID = '.$DB->quote( $Blog->ID ) );
           
$user_perms_SQL->WHERE_and( 'bloguser_can_be_assignee <> 0' );
           
$users_sql[] = $user_perms_SQL->get();

           
// Get users which primary groups can be assignees of the collection:
           
$group_perms_SQL = new SQL();
           
$group_perms_SQL->SELECT( 'user_login' );
           
$group_perms_SQL->FROM( 'T_users' );
           
$group_perms_SQL->FROM_add( 'INNER JOIN T_coll_group_perms ON user_grp_ID = bloggroup_group_ID' );
           
$group_perms_SQL->WHERE( 'bloggroup_blog_ID = '.$DB->quote( $Blog->ID ) );
           
$group_perms_SQL->WHERE_and( 'bloggroup_can_be_assignee <> 0' );
           
$users_sql[] = $group_perms_SQL->get();

           
// Get users which secondary groups can be assignees of the collection:
           
$secondary_group_perms_SQL = new SQL();
           
$secondary_group_perms_SQL->SELECT( 'user_login' );
           
$secondary_group_perms_SQL->FROM( 'T_users' );
           
$secondary_group_perms_SQL->FROM_add( 'INNER JOIN T_users__secondary_user_groups ON sug_user_ID = user_ID' );
           
$secondary_group_perms_SQL->FROM_add( 'INNER JOIN T_coll_group_perms ON sug_grp_ID = bloggroup_group_ID' );
           
$secondary_group_perms_SQL->WHERE( 'bloggroup_blog_ID = '.$DB->quote( $Blog->ID ) );
           
$secondary_group_perms_SQL->WHERE_and( 'bloggroup_can_be_assignee <> 0' );
           
$users_sql[] = $secondary_group_perms_SQL->get();
        }

       
// Get collection's owner because it can be assignee by default:
       
$owner_SQL = new SQL();
       
$owner_SQL->SELECT( 'user_login' );
       
$owner_SQL->FROM( 'T_users' );
       
$owner_SQL->FROM_add( 'INNER JOIN T_blogs ON blog_owner_user_ID = user_ID' );
       
$owner_SQL->WHERE( 'blog_ID = '.$DB->quote( $Blog->ID ) );
       
$users_sql[] = $owner_SQL->get();

       
// Get assignees which primary groups have a setting to EDIT ALL collections:
       
$group_setting_SQL = new SQL();
       
$group_setting_SQL->SELECT( 'user_login' );
       
$group_setting_SQL->FROM( 'T_users' );
       
$group_setting_SQL->FROM_add( 'INNER JOIN T_groups ON user_grp_ID = grp_ID' );
       
$group_setting_SQL->WHERE( 'grp_perm_blogs = "editall"' );
       
$users_sql[] = $group_setting_SQL->get();

       
// Union sql queries to execute one query and save an order as one list:
       
$users_sql = 'SELECT user_login'
           
.' FROM ( ( '.implode( ' ) UNION ( ', $users_sql ).' ) ) AS uu'
           
.' WHERE uu.user_login LIKE "'.$DB->escape( $api_q ).'%"'
           
.' ORDER BY user_login'
           
.' LIMIT 10';

       
$user_logins = $DB->get_col( $users_sql );

       
// Send users logins array as response:
       
$this->add_response( 'list', $user_logins );
    }


   
/**
     * Call collection controller to toggle favorite status
     *
     * Request scheme: "<baseurl>/api/v1/collections/<collname>/favorite
     *
     */
   
private function controller_coll_favorite()
    {
        global
$current_User, $Collection, $Blog;

        if( !
is_logged_in() )
        {    
// Check permission: Current user must be logged in
           
$this->halt( 'You are not allowed to set the collection "'.$Blog->get( 'name' ).'" as a favorite.', 'no_access', 403 );
           
// Exit here.
       
}

       
parse_str( file_get_contents( 'php://input' ), $data );
       
$setting = param_format( $data['setting'], 'integer' );

       
$r = $Blog->favorite( $current_User->ID, $setting );

        if(
is_null( $r ) )
        {
           
$this->add_response( 'status', 'fail', 'string' );
           
$this->add_response( 'errorMsg', T_('Unable to set collection favorite status') );
        }
        else
        {
           
$this->add_response( 'status', 'ok', 'string' );
           
$this->add_response( 'setting', $setting );
        }
    }


   
/**
     * Call item controller to flag by current user
     *
     * @param integer Item ID
     */
   
private function controller_coll_items_flag( $item_ID )
    {
        global
$Collection, $Blog;

       
$ItemCache = & get_ItemCache();
        if( (
$Item = & $ItemCache->get_by_ID( $item_ID, false, false ) ) === false )
        {    
// Item is not detected in DB by requested ID:
           
$this->halt( 'No item found in DB by requested ID #'.$item_ID, 'unknown_item', 404 );
           
// Exit here.
       
}

        if(
$Item->get_blog_ID() != $Blog->ID )
        {    
// Item should be called for current collection:
           
$this->halt( 'You request item #'.$Item->ID.' from another collection "'.$Blog->get( 'urlname' ).'"', 'wrong_item_coll', 403 );
           
// Exit here.
       
}

        if( !
$Item->can_flag() )
        {    
// Don't display the flag button if it is not allowed by some reason:
           
$this->halt( 'You cannot flag the item #'.$Item->ID, 'cannot_flag_item', 403 );
        }

       
// Flag or unflag item for current user:
       
$Item->update_flag();

       
// Return current state of flag:
       
$this->add_response( 'flag', $Item->get_user_data( 'item_flag' ), 'integer' );
    }


   
/**** MODULE COLLECTIONS ---- END ****/


    /**** MODULE USERS ---- START ****/


    /**
     * Call module to prepare request for users
     */
   
private function module_users()
    {
        switch(
$_SERVER['REQUEST_METHOD'] )
        {
            case
'GET':
               
// Check for valid controllers
               
$user_controller = '';
               
$user_ID = NULL;

                if( ! empty(
$this->args[1] ) )
                {
                    if( ! empty(
$this->args[2] ) )
                    {
                       
$user_controller = $this->args[2];
                    }
                    else
                    {
                        if(
preg_match( '/^\d+$/', $this->args[1] ) )
                        {
// args[1] should be a positive integer and not just any number:
                           
$user_ID = intval( $this->args[1] );
                           
$user_controller = 'view';
                        }
                        else
                        {
                           
$user_controller = $this->args[1];
                        }
                    }
                }
                else
                {
// default handler
                   
$user_controller = '';
                }

               
$valid_resources = array( '', 'view', 'recipients', 'authors', 'autocomplete', 'logins', 'search' );
                if( isset(
$user_ID ) )
                {
// Set controller to view the requested user profile:
                   
$default_controller = 'view';
                }
                else
                {
                   
$default_controller = $user_controller;
                }
                break;

            case
'POST':
               
// check for valid controllers
               
$valid_resources = array( 'save' );
               
$default_controller = 'save';
                break;

            case
'DELETE':
                if( empty(
$this->args[1] ) )
                {
                   
$this->halt( 'Missing user ID', 'user_missing_id', 400 );
                   
// Exit here
               
}

               
$valid_resources = array( 'delete' );
               
$default_controller = 'delete';
                break;

            default:
               
$this->halt( 'Request method not supported for the requested resource', 'wrong_request', 405 );
               
// Exit here
       
}

       
$user_controller = $default_controller;

        if( !
method_exists( $this, 'controller_user_'.$user_controller ) )
        {    
// Unknown controller:
           
$this->halt( 'Unknown user controller "'.$user_controller.'"', 'unknown_controller' );
           
// Exit here.
       
}

        if( !
in_array( $user_controller, $valid_resources, true ) )
        {
// Invalid request method:
           
$this->halt( 'Request method not supported for the requested resource', 'wrong_request', 405 );
           
// Exit here.
       
}

       
// Call collection controller to prepare current request:
       
$this->{'controller_user_'.$user_controller}();
    }


   
/**
     * Default controller/handler
     */
   
private function controller_user_()
    {
        global
$Settings;

       
$api_restrict_to_available_fileroots = param( 'restrict_to_available_fileroots', 'integer', 0 ); // 1 - Load only users with available file roots for current user

       
if( $api_restrict_to_available_fileroots )
        {    
// Check if current user has an access to file roots of other users:
           
global $current_User;
            if(
is_logged_in() )
            {    
// Check perms for logged in user:
               
if( ! ( check_user_perm( 'users', 'moderate' ) && check_user_perm( 'files', 'all' ) ) )
                {    
// Current user has an access only to file root of own account:
                   
$user_filters = array( 'userids' => array( $current_User->ID ) );
                }
               
// otherwise current user has an access to file roots of all users
           
}
            else
            {    
// Anonymous user has no access to file roots:
               
$user_filters = array( 'userids' => array( -1 ) );
            }
        }
        else
        {    
// Default restriction:
           
if( ( $access_error_message = check_access_users_list( 'api' ) ) !== true )
            {    
// Current user has no access to public list of the users,
                // Display error message:
               
$this->halt( $access_error_message, 'no_access', 403 );
               
// Exit here.
           
}
        }

       
// Get param to limit number users per page:
       
$api_per_page = param( 'per_page', 'integer', 10 );

       
// Get param to select current page:
        // (NOTE: if this param is not set then param 'paged' is used as filter of UserList)
       
$page = param( 'page', 'integer', NULL );

       
// Get user list params:
       
$api_list_params = param( 'list_params', 'array:string', array() );

       
// Alias for filter param 'keywords':
       
$api_q = param( 'q', 'string', NULL );
        if(
$api_q !== NULL )
        {
           
set_param( 'keywords', $api_q );
        }

       
// Create result set:
       
load_class( 'users/model/_userlist.class.php', 'UserList' );
       
$UserList = new UserList( 'api_', $api_per_page, '', $api_list_params );
       
$UserList->load_from_Request();

        if( ! empty(
$user_filters ) )
        {    
// Filter list:
           
$UserList->set_filters( $user_filters );
        }

        if(
$page !== NULL )
        {    
// Set page from request:
           
$UserList->page = $page;
        }

       
// Execute query:
       
$UserList->query();

       
// Add data of users list to response:
       
$this->add_response( 'found', $UserList->total_rows, 'integer' );
       
$this->add_response( 'page', $UserList->page, 'integer' );
       
$this->add_response( 'page_size', $UserList->limit, 'integer' );
       
$this->add_response( 'pages_total', $UserList->total_pages, 'integer' );

       
// Add each user row in the response array:
       
while( $User = & $UserList->get_next() )
        {
           
// ID:
           
$user_data = array( 'id' => intval( $User->ID ) );
           
// Picture:
           
if( $Settings->get( 'allow_avatars' ) )
            {    
// Only if it is allowed by general settings:
               
$user_picture = $User->get_avatar_imgtag( 'crop-top-48x48' );
                if(
preg_match( '#src="([^"]+)"#', $user_picture, $user_picture ) )
                {
                   
$user_data['picture'] = $user_picture[1];
                }
            }
           
// Login:
           
$user_data['login'] = $User->get( 'login' );
           
// Full name:
           
$user_data['fullname'] = $User->get( 'fullname' );
           
// City:
           
$user_data['city'] = $User->get_city_name();

           
// Add each user row in the response array:
           
$this->add_response( 'users', $user_data, 'array' );
        }

        if( empty(
$user_data ) )
        {    
// No users found:
           
$this->halt( 'No users found', 'no_users', 404 );
           
// Exit here.
       
}
    }


   
/**
     * Call user controller to view a profile of the requested user
     */
   
private function controller_user_view()
    {
       
// Get an user ID for request "GET <baseurl>/api/v1/users/<id>":
       
$user_ID = intval( empty( $this->args[1] ) ? 0 : $this->args[1] );

        if( (
$access_error_message = check_access_user_profile( $user_ID, 'api' ) ) !== true )
        {    
// Current user has no access to public list of the users,
            // Display error message:
           
$this->halt( $access_error_message, 'no_access', 403 );
           
// Exit here.
       
}

       
$UserCache = & get_UserCache();
       
$User = & $UserCache->get_by_ID( $user_ID, false, false );

        if( !
$User )
        {    
// Wrong user request:
           
$this->halt( 'Invalid user ID', 'user_invalid_id', 404 );
           
// Exit here.
       
}

       
// ID:
       
$user_data = array( 'id' => intval( $User->ID ) );
       
// Pictures:
        // Main Picture:
       
$user_picture = $User->get_avatar_imgtag( is_logged_in() ? 'crop-top-320x320' : 'crop-top-320x320-blur-8' );
       
$user_picture = ( preg_match( '#src="([^"]+)"#', $user_picture, $user_picture ) ) ? $user_picture[1] : '';
       
$user_data['picture'] = $user_picture;
       
// Other pictures:
       
$user_data['pictures'] = array();
        if(
check_user_status( 'can_view_user', $user_ID ) )
        {
// Display other pictures, but only for logged in and activated users:
           
$user_pic_links = $User->get_avatar_Links();
            foreach(
$user_pic_links as $user_pic_Link )
            {
               
$user_pic_url = $user_pic_Link->get_tag( array(
                       
'before_image'        => '',
                       
'before_image_legend' => NULL,
                       
'after_image_legend'  => NULL,
                       
'after_image'         => '',
                       
'image_size'          => 'crop-top-80x80',
                    ) );
                if( (
preg_match( '#src="([^"]+)"#', $user_pic_url, $user_pic_url ) ) )
                {    
// Extract image url:
                   
$user_data['pictures'][] = $user_pic_url[1];
                }
            }
        }

       
// Login:
       
$user_data['login'] = $User->get( 'login' );
       
// First name:
       
$user_data['firstname'] = $User->get( 'firstname' );
       
// Last name:
       
$user_data['lastname'] = $User->get( 'lastname' );
       
// Gender:
       
$user_data['gender'] = $User->get( 'gender' );

       
// Location:
       
$user_data['location'] = array(
               
'country'   => empty( $User->ctry_ID )  ? NULL : array( 'id' => intval( $User->ctry_ID ),  'name' => $User->get_country_name() ),
               
'region'    => empty( $User->rgn_ID )   ? NULL : array( 'id' => intval( $User->rgn_ID ),   'name' => $User->get_region_name() ),
               
'subregion' => empty( $User->subrg_ID ) ? NULL : array( 'id' => intval( $User->subrg_ID ), 'name' => $User->get_subregion_name() ),
               
'city'      => empty( $User->city_ID )  ? NULL : array( 'id' => intval( $User->city_ID ),  'name' => $User->get_city_name() )
            );

       
// Organizations:
       
$user_data['organizations'] = array();
       
$user_organizations = $User->get_organizations();
        foreach(
$user_organizations as $org )
        {
           
$user_data['organizations'][] = array(
                   
'name' => $org->name,
                   
'url'  => $org->url,
                );
        }

       
// User fields:
       
$user_data['userfields'] = array();
       
// Load the user fields:
       
$User->userfields_load();
        foreach(
$User->userfields as $userfield )
        {
            if( ! isset(
$user_data['userfields'][ $userfield->ufgp_name ] ) )
            {    
// Initalize an array for each group of the user fields:
               
$user_data['userfields'][ $userfield->ufgp_name ] = array();
            }

           
$user_data['userfields'][ $userfield->ufgp_name ][] = array(
                   
'code'  => $userfield->ufdf_code,
                   
'name'  => $userfield->ufdf_name,
                   
'value' => $userfield->uf_varchar
               
);
        }

       
// Add user data in the response:
       
$this->add_response( NULL, $user_data );
    }


   
/**
     * Call user controller to update the requested user OR create new one
     */
   
private function controller_user_save()
    {
        global
$current_User;

        if( !
is_logged_in() )
        {    
// Must be logged in
           
$this->halt( T_( 'You are not logged in.' ), 'no_access', 403 );
           
// Exit here.
       
}

       
// Get an user ID for request "POST <baseurl>/api/v1/users/<id>":
       
$user_ID = empty( $this->args[1] ) ? 0 : intval( $this->args[1] );

        if(
$user_ID > 0 )
        {    
// Initialize User object to update it:
           
$UserCache = & get_UserCache();
           
$edited_User = & $UserCache->get_by_ID( $user_ID, false, false );

            if( !
$edited_User )
            {    
// Wrong user request:
               
$this->halt( 'Invalid user ID', 'user_invalid_id' );
               
// Exit here.
           
}

            if( !
$current_User->can_moderate_user( $edited_User->ID )
                &&
$edited_User->ID != $current_User->ID )
            {    
// Current user has no permission to delate the requested user:
               
$this->halt( T_('You have no permission to edit other users!'), 'no_access', 403 );
               
// Exit here.
           
}
        }
        else
        {
// Initialize User object to create new one:
           
$edited_User = new User();
        }

       
// Clear all messages in order to keep only from function User->update_from_request() below:
       
global $Messages;
       
$Messages->clear();

       
// Create new user or Update the requested user:
       
$is_new_user = ( $edited_User->ID == 0 ? true : false );
       
$result = $edited_User->update_from_request( $is_new_user );
        if(
$result !== true )
        {    
// There are errors on update the requested user:
           
$Messages->close_group(); // Make sure any open message group are closed
           
$this->halt( $Messages->messages_text[0], 'update_failed', 403 );
           
// Exit here.
       
}
        else
        {    
// The requested user has been updated successfully
           
$Messages->close_group(); // Make sure any open message group are closed
           
$this->halt( $Messages->messages_text[0], 'update_success', ( $is_new_user ? 201 : 200 ) );
           
// Exit here.
       
}
    }


   
/**
     * Call user controller to delete the requested user
     */
   
private function controller_user_delete()
    {
        global
$current_User;

        if( !
check_user_perm( 'users', 'edit' ) )
        {    
// Current user has no permission to delete the requested user:
           
$this->halt( T_('You have no permission to edit other users!'), 'no_access', 403 );
           
// Exit here.
       
}

       
// Get an user ID for request "DELETE <baseurl>/api/v1/users/<id>":
       
$user_ID = intval( $this->args[1] );

       
$UserCache = & get_UserCache();
       
$User = & $UserCache->get_by_ID( $user_ID, false, false );

        if( !
$User )
        {    
// Wrong user request:
           
$this->halt( 'Invalid user ID', 'user_invalid_id', 404 );
           
// Exit here.
       
}

        if(
$User->ID == $current_User->ID )
        {
           
$this->halt( T_('You can\'t delete yourself!'), 'no_access', 403 );
           
// Exit here.
       
}
        if(
$User->ID == 1 )
        {
           
$this->halt( T_('You can\'t delete User #1!'), 'no_access', 403 );
           
// Exit here.
       
}

       
// Clear all messages in order to keep only from function User->check_delete() below:
       
global $Messages;
       
$Messages->clear();

        if( !
$User->check_delete( sprintf( T_('Cannot delete User &laquo;%s&raquo;'), $User->get( 'login' ) ) ) )
        {    
// There are restrictions on delete the requested user:
           
$Messages->close_group(); // Make sure any open message group are closed
           
$this->halt( strip_tags( $Messages->messages_text[0] ), 'delete_restriction', 403 );
           
// Exit here.
       
}

        if(
$User->dbdelete( $Messages ) !== false )
        {    
// The requested user has been deleted successfully
           
$this->halt( sprintf( T_('User &laquo;%s&raquo; deleted.'), $User->get( 'login' ) ), 'delete_success', 200 );
           
// Exit here.
       
}
        else
        {    
// Cannot delete the requested user
           
$this->halt( sprintf( T_('Cannot delete User &laquo;%s&raquo;'), $User->get( 'login' ) ), 'delete_failed', 403 );
           
// Exit here.
       
}
    }


   
/**
     * Call user controller to search recipients
     */
   
private function controller_user_recipients()
    {
        global
$current_User, $DB;

        if( !
check_user_perm( 'perm_messaging', 'reply' ) )
        {    
// Check permission: User is not allowed to view threads
           
$this->halt( 'You are not allowed to view recipients.', 'no_access', 403 );
           
// Exit here.
       
}

        if(
check_create_thread_limit( true ) )
        {    
// User has already reached his limit, don't allow to get a users list:
           
$this->halt_with_Messages();
        }

       
$api_q = param( 'q', 'string', '' );

       
// Search users:
       
$users = $this->func_user_search( $api_q, array(
               
'sql_where' => 'user_ID != '.$DB->quote( $current_User->ID ),
               
'sql_mask'  => '%$login$%',
            ) );

        foreach(
$users as $User )
        {
            if( !
$User->check_status( 'can_receive_pm' ) )
            {    
// This user is probably closed so don't show it:
               
continue;
            }

           
$user_data = array(
                   
'id'       => $User->ID,
                   
'login'    => $User->get( 'login' ),
                   
'fullname' => $User->get( 'fullname' ),
                   
'avatar'   => $User->get_avatar_imgtag( 'crop-top-32x32' ),
                );

           
// Add data of each user in separate array of response:
           
$this->add_response( 'users', $user_data, 'array' );
        }
    }


   
/**
     * Call user controller to search for authors
     */
   
private function controller_user_authors()
    {
        global
$current_User, $DB;

       
$api_q = param( 'q', 'string', '' );

        if( !
is_logged_in() )
        {
           
$this->halt( 'You are not allowed to view users.', 'no_access', 403 );
           
// Exit here.
       
}

       
// Search users:
       
$users = $this->func_user_search( $api_q, array(
               
'sql_where' => 'user_ID != '.$DB->quote( $current_User->ID ),
               
'sql_mask'  => '%$login$%',
            ) );

        foreach(
$users as $User )
        {
           
$user_data = array(
                   
'id'       => $User->ID,
                   
'login'    => $User->get( 'login' ),
                   
'fullname' => $User->get( 'fullname' ),
                   
'avatar'   => $User->get_avatar_imgtag( 'crop-top-32x32' ),
                );

           
// Add data of each user in separate array of response:
           
$this->add_response( 'users', $user_data, 'array' );
        }
    }


   
/**
     * Call user controller to search user for autocomplete JS plugin
     */
   
private function controller_user_autocomplete()
    {
        global
$DB;

       
$api_q = param( 'q', 'string', '' );
       
$api_mentioned = param( 'mentioned', 'array:string' ); // User logins mentioned on the page
       
$api_blog = param( 'blog', 'integer' );

        if(
is_valid_login( $api_q ) !== true )
        {    
// Restrict a wrong request:
           
$this->halt( 'Wrong request', 'wrong_request', 403 );
           
// Exit here.
       
}

       
// Add backslash for special char of sql operator LIKE:
       
$api_q = str_replace( '_', '\_', $api_q );

       
$search_params = array();
       
$order_priorities = array();

        if( ! empty(
$api_mentioned ) )
        {    
// Mentioned logins must be ordered on the top:
           
$order_priorities[] = 'WHEN user_login IN ( '.$DB->quote( $api_mentioned ).' ) THEN 1';
        }

        if( ! empty(
$api_blog ) )
        {    
// Collection assignees and members must be ordered with priorities 2 and 3:
           
$search_params['sql_from_add'] = 'LEFT JOIN T_coll_user_perms ON bloguser_user_ID = user_ID AND bloguser_blog_ID = '.$DB->quote( $api_blog ).'
                LEFT JOIN T_coll_group_perms ON bloggroup_group_ID = user_grp_ID AND bloggroup_blog_ID = '
.$DB->quote( $api_blog );
           
$order_priorities[] = 'WHEN bloguser_can_be_assignee = 1 OR bloggroup_can_be_assignee = 1 THEN 2';
           
$order_priorities[] = 'WHEN bloguser_ismember = 1 OR bloggroup_ismember = 1  THEN 3';
        }

        if( ! empty(
$order_priorities ) )
        {    
// Order users by custom priority:
           
$search_params['sql_select'] = '*, CASE '.implode( ' ', $order_priorities ).' ELSE 4 END AS user_order_priority';
           
$search_params['sql_order_by'] = 'user_order_priority, user_login';
        }

       
// Search users:
       
$users = $this->func_user_search( $api_q, $search_params );

        foreach(
$users as $User )
        {
           
$user_data = array(
                   
'login'    => $User->get( 'login' ),
                );

           
// Add data of each user in separate array of response:
           
$this->add_response( 'users', $user_data, 'array' );
        }
    }


   
/**
     * Call user controller to search user for hintbox and typeahead JS plugins
     */
   
private function controller_user_logins()
    {
       
$api_q = trim( urldecode( param( 'q', 'string', '' ) ) );
       
$api_status = param( 'status', 'string', '' );

       
/**
         * sam2kb> The code below decodes percent-encoded unicode string produced by Javascript "escape"
         * function in format %uxxxx where xxxx is a Unicode value represented as four hexadecimal digits.
         * Example string "MAMA" (cyrillic letters) encoded with "escape": %u041C%u0410%u041C%u0410
         * Same word encoded with "encodeURI": %D0%9C%D0%90%D0%9C%D0%90
         *
         * jQuery hintbox plugin uses "escape" function to encode URIs
         *
         * More info here: http://en.wikipedia.org/wiki/Percent-encoding#Non-standard_implementations
         */
       
if( preg_match( '~%u[0-9a-f]{3,4}~i', $api_q ) )
        {    
// Decode UTF-8 string:
           
$api_q = preg_replace( '~%u([0-9a-f]{3,4})~i', '&#x\\1;', $api_q );
           
$api_q = html_entity_decode( $api_q, ENT_COMPAT, 'UTF-8' );
        }

        if( empty(
$api_q ) )
        {    
// Don't allow empty request:
           
$this->halt( 'Please enter at least one char to find assignees', 'no_access', 403 );
           
// Exit here.
       
}

       
$func_user_search_params = array( 'sql_limit' => 10 );
        if(
$api_status == 'all' )
        {    
// Get users with all statuses:
           
$func_user_search_params['sql_where'] = '';
        }
        elseif( ! empty(
$api_status ) )
        {    
// Restrict users with requested statuses:
           
global $DB;
           
$func_user_search_params['sql_where'] = 'user_status IN ( '.$DB->quote( explode( ',', $api_status ) ).' )';
        }

       
// Search users:
       
$users = $this->func_user_search( $api_q, $func_user_search_params );

       
// Check if current user can see other users with ALL statuses:
       
$can_view_all_users = check_user_perm( 'users', 'view' );

       
$user_logins = array();
        foreach(
$users as $User )
        {
            if(
$can_view_all_users || in_array( $User->get( 'status' ), array( 'activated', 'autoactivated', 'manualactivated' ) ) )
            {    
// Allow to see this user only if current User has a permission to see users with current status:
               
$user_logins[] = $User->get( 'login' );
            }
        }

       
// Send users logins array as response:
       
$this->add_response( 'list', $user_logins );
    }


   
/**
     * Function to search users by login
     *
     * @param string Search string
     * @return array Users
     */
   
private function func_user_search( $search_string, $params = array() )
    {
        global
$DB;

       
$params = array_merge( array(
               
'sql_select'   => '*',
               
'sql_from_add' => '',
               
'sql_where'    => 'user_status IN ( "activated", "autoactivated", "manualactivated" )',
               
'sql_mask'     => '$login$%',
               
'sql_limit'    => 0,
               
'sql_order_by' => 'user_login',
            ),
$params );

       
// Get request params:
       
$api_page = param( 'page', 'integer', 1 );
       
$api_per_page = param( 'per_page', 'integer', $params['sql_limit'] );

       
// Initialize SQL to get users:
       
$users_SQL = new SQL();
       
$users_SQL->SELECT( $params['sql_select'] );
       
$users_SQL->FROM( 'T_users' );
        if( ! empty(
$params['sql_from_add'] ) )
        {    
// Additional tables:
           
$users_SQL->FROM_add( $params['sql_from_add'] );
        }
        if( ! empty(
$search_string ) )
        {    
// Filter by login:
           
$users_SQL->WHERE( 'user_login LIKE '.$DB->quote( str_replace( '$login$', $search_string, $params['sql_mask'] ) ) );
        }
        if( ! empty(
$params['sql_where'] ) )
        {    
// Additional restrict:
           
$users_SQL->WHERE_and( $params['sql_where'] );
        }
       
$users_SQL->ORDER_BY( $params['sql_order_by'] );

       
// Get a count of users:
       
$count_users = $DB->get_var( preg_replace( '/SELECT(.+)FROM/i', 'SELECT COUNT( user_ID ) FROM', $users_SQL->get() ) );

       
// Set page params:
       
$count_pages = empty( $api_per_page ) ? 1 : ceil( $count_users / $api_per_page );
        if( empty(
$api_page ) )
        {    
// Force wrong page number to first:
           
$api_page = 1;
        }
        if(
$api_page > $count_pages )
        {    
// Don't allow page number more than total pages:
           
$api_page = $count_pages;
        }
        if(
$api_per_page > 0 )
        {    
// Limit users by current page:
           
$users_SQL->LIMIT( ( ( $api_page - 1 ) * $api_per_page ).', '.$api_per_page );
        }

       
// Load users in cache by SQL:
       
$UserCache = & get_UserCache();
       
$UserCache->clear();
       
$UserCache->load_by_sql( $users_SQL );

        if( empty(
$UserCache->cache ) )
        {    
// No users found:
           
$this->halt( 'No users found', 'no_users', 200 );
           
// Exit here.
       
}

       
$this->add_response( 'found', $count_users, 'integer' );
       
$this->add_response( 'page', $api_page, 'integer' );
       
$this->add_response( 'page_size', $api_per_page, 'integer' );
       
$this->add_response( 'pages_total', $count_pages, 'integer' );

        return
$UserCache->cache;
    }


   
/**** MODULE USERS ---- END ****/


    /**** MODULE TAGS ---- START ****/


    /**
     * Call module to prepare request for tags
     */
   
private function module_tags()
    {
        global
$DB;

       
$term = param( 's', 'string' );

        if(
substr( $term, 0, 1 ) == '-' )
        {    
// Prevent chars '-' in first position:
           
$term = preg_replace( '/^-+/', '', $term );
        }

       
// Deny to use a comma in tag names:
       
$term = str_replace( ',', ' ', $term );

       
$term_is_new_tag = true;

       
$tags = array();

       
$tags_SQL = new SQL();
       
$tags_SQL->SELECT( 'tag_name AS id, tag_name AS name' );
       
$tags_SQL->FROM( 'T_items__tag' );
       
/* Yura: Here I added "COLLATE utf8_general_ci" because:
         * It allows to match "testA" with "testa", and otherwise "testa" with "testA".
         * It also allows to find "ee" when we type in "éè" and otherwise.
         *
         * Erwin: Changed added collation from "utf8_general_ci" to "utf8mb4_general_ci" to match default DB connection collation
         */
       
$tags_SQL->WHERE( 'tag_name LIKE '.$DB->quote( '%'.$term.'%' ).' COLLATE utf8mb4_general_ci' );
       
$tags_SQL->ORDER_BY( 'tag_name' );
       
$tags = $DB->get_results( $tags_SQL->get(), ARRAY_A );

       
// Check if current term is not an existing tag:
       
foreach( $tags as $tag )
        {
           
/* Yura: I have added "utf8_strtolower()" below in condition in order to:
             * When we enter new tag 'testA' and the tag 'testa' already exists
             * then we suggest only 'testa' instead of 'testA'.
             */
           
if( utf8_strtolower( $tag['name'] ) == utf8_strtolower( $term ) )
            {
// Current term is an existing tag
               
$term_is_new_tag = false;
            }
        }

        if(
$term_is_new_tag && ! empty( $term ) )
        {    
// Add current term in the beginning of the tags list:
           
array_unshift( $tags, array( 'id' => $term, 'name' => $term ) );
        }

       
$this->add_response( 'tags', $tags );
    }


   
/**** MODULE TAGS ---- END ****/

    /**** MODULE USER TAGS ---- START ****/


    /**
     * Call module to prepare request for user tags
     */
   
private function module_usertags()
    {
        global
$DB;

       
$term = param( 's', 'string' );

        if(
substr( $term, 0, 1 ) == '-' )
        {    
// Prevent chars '-' in first position:
           
$term = preg_replace( '/^-+/', '', $term );
        }

       
// Deny to use a comma in tag names:
       
$term = str_replace( ',', ' ', $term );

       
$term_is_new_tag = true;

       
$tags = array();

       
$tags_SQL = new SQL();
       
$tags_SQL->SELECT( 'utag_name AS id, utag_name AS name' );
       
$tags_SQL->FROM( 'T_users__tag' );
       
/* Yura: Here I added "COLLATE utf8_general_ci" because:
         * It allows to match "testA" with "testa", and otherwise "testa" with "testA".
         * It also allows to find "ee" when we type in "éè" and otherwise.
         *
         * Erwin: Changed added collation from "utf8_general_ci" to "utf8mb4_general_ci" to match default DB connection collation
         */
       
$tags_SQL->WHERE( 'utag_name LIKE '.$DB->quote( '%'.$term.'%' ).' COLLATE utf8mb4_general_ci' );
       
$tags_SQL->ORDER_BY( 'utag_name' );
       
$tags = $DB->get_results( $tags_SQL->get(), ARRAY_A );

       
// Check if current term is not an existing tag:
       
foreach( $tags as $tag )
        {
           
/* Yura: I have added "utf8_strtolower()" below in condition in order to:
             * When we enter new tag 'testA' and the tag 'testa' already exists
             * then we suggest only 'testa' instead of 'testA'.
             */
           
if( utf8_strtolower( $tag['name'] ) == utf8_strtolower( $term ) )
            {
// Current term is an existing tag
               
$term_is_new_tag = false;
            }
        }

        if(
$term_is_new_tag && ! empty( $term ) )
        {    
// Add current term in the beginning of the tags list:
           
array_unshift( $tags, array( 'id' => $term, 'name' => $term ) );
        }

       
$this->add_response( 'tags', $tags );
    }


   
/**** MODULE USER TAGS ---- END ****/

    /**** MODULE POLLS ---- START ****/

   
private function module_polls()
    {
        global
$DB, $current_User;

        if(
is_logged_in() && $current_User )
        {
           
$polls = array();

           
$perm_poll_view = check_user_perm( 'polls', 'view' );

           
$polls_SQL = new SQL();
           
$polls_SQL->SELECT( 'pqst_ID, pqst_owner_user_ID, pqst_question_text' );
           
$polls_SQL->FROM( 'T_polls__question' );
            if( !
$perm_poll_view )
            {
               
$polls_SQL->WHERE( 'pqst_owner_user_ID = '.$DB->quote( $current_User->ID ) );
            }

           
$poll_count_SQL = new SQL();
           
$poll_count_SQL->SELECT( 'COUNT( pqst_ID )' );
           
$poll_count_SQL->FROM( 'T_polls__question' );
            if( !
$perm_poll_view )
            {
               
$poll_count_SQL->WHERE( 'pqst_owner_user_ID = '.$DB->quote( $current_User->ID ) );
            }

           
$polls = $DB->get_results( $polls_SQL->get(), ARRAY_A );

           
$this->add_response( 'polls', $polls );
        }
        else
        {
           
$this->halt( 'You are not allowed to view polls.', 'no_access', 403 );
        }
    }

   
/**** MODULE POLLS ---- END ****/


    /**** MODULE LINKS ---- START ****/


    /**
     * Call module to prepare request for links
     */
   
private function module_links()
    {
        if( ! empty(
$this->args[2] ) )
        {    
// Get action from request string:
           
$link_action = $this->args[2];
        }
        else
        {    
// Get action from param:
           
$link_action = param( 'action', 'string', '' );
        }

       
// Set link controller '' by default:
       
$link_controller = '';

       
$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? $_SERVER['REQUEST_METHOD'] : 'GET';
        switch(
$request_method )
        {
            case
'DELETE':
               
// Set controller to unlink/delete the requested link:
               
$link_controller = 'delete';
                break;

            case
'GET':
            case
'POST':
               
// Actions to update the links:
               
switch( $link_action )
                {
                    case
'position':
                       
$link_controller = 'change_position';
                        break;

                    case
'move_up':
                    case
'move_down':
                       
$link_controller = 'change_order';
                        break;

                    case
'attach':
                       
$link_controller = 'attach';
                        break;

                    case
'refresh':
                    case
'sort':
                       
$link_controller = 'refresh';
                        break;

                    case
'copy':
                       
$link_controller = 'copy';
                        break;
                }
                break;
        }

        if( !
method_exists( $this, 'controller_link_'.$link_controller ) )
        {    
// Unknown controller:
           
$this->halt( 'Unknown link controller "'.$link_controller.'"', 'unknown_controller' );
           
// Exit here.
       
}

       
// Call collection controller to prepare current request:
       
$this->{'controller_link_'.$link_controller}();
    }


   
/**
     * Get Link object
     *
     * @return object Link
     */
   
private function & get_Link()
    {
       
// Get link ID:
       
$link_ID = empty( $this->args[1] ) ? 0 : intval( $this->args[1] );

       
$LinkCache = & get_LinkCache();
       
$Link = & $LinkCache->get_by_ID( $link_ID, false, false );

        return
$Link;
    }


   
/**
     * Check permission if current user can update the requested link
     */
   
private function link_check_perm()
    {
        if( ! (
$Link = & $this->get_Link() ) )
        {    
// Wrong link request:
           
$this->halt( 'Invalid link ID', 'link_invalid_id' );
           
// Exit here.
       
}

       
$LinkOwner = & $Link->get_LinkOwner();

        if( !
$LinkOwner->check_perm( 'edit', false ) )
        {    
// Current user has no permission to unlink the requested link:
           
$this->halt( 'You have no permission to edit the requested link!', 'no_access', 403 );
           
// Exit here.
       
}
    }


   
/**
     * Call link controller to unlink the requested link or delete file completely
     */
   
private function controller_link_delete()
    {
       
// Check permission if current user can update the requested link:
       
$this->link_check_perm();

       
// Action: 'unlink - just unlink file from the owner, 'delete' - unlink and delete the file from disk and DB completely

        // Note: param() currently does not work with DELETE and PUT requests
       
parse_str( file_get_contents('php://input'), $request_params );
       
$action = isset( $request_params['action'] ) ? $request_params['action'] : '';

       
$deleted_Link = & $this->get_Link();
       
$LinkOwner = & $deleted_Link->get_LinkOwner();

        if(
$link_File = & $deleted_Link->get_File() )
        {
           
syslog_insert( sprintf( 'File %s was unlinked from %s with ID=%s', '[['.$link_File->get_name().']]', $LinkOwner->type, $LinkOwner->get_ID() ), 'info', 'file', $link_File->ID );
        }

        if(
$action == 'delete' && $deleted_Link->can_be_file_deleted() )
        {    
// Get a linked file to delete it after unlinking if it is allowed for current user:
           
$linked_File = & $deleted_Link->get_File();
        }

       
// Unlink File from Item/Comment:
       
$deleted_link_ID = $deleted_Link->ID;
        if(
$LinkOwner->remove_link( $deleted_Link ) )
        {    
// If Link has been removed successfully:

           
$LinkOwner->after_unlink_action( $deleted_link_ID );

            if(
$action == 'delete' && ! empty( $linked_File ) )
            {    
// Delete a linked file from disk and DB completely:
               
$linked_File->unlink();
            }

           
// The requested link has been deleted successfully:
           
$this->halt( $LinkOwner->translate( 'Link has been deleted from $xxx$.' ), 'delete_success', 200 );
           
// Exit here.
       
}
        else
        {    
// The requested link cannot be deleted:
           
$this->halt( $LinkOwner->translate( 'Cannot delete Link from $xxx$.' ), 'delete_failed', 403 );
           
// Exit here.
       
}
    }


   
/**
     * Call link controller to change the position of the requested link
     */
   
private function controller_link_change_position()
    {
        global
$DB, $Session;

       
// Check permission if current user can update the requested link:
       
$this->link_check_perm();

       
$link_position = $this->args[3];

       
$edited_Link = & $this->get_Link();
       
$LinkOwner = & $edited_Link->get_LinkOwner();

       
// Don't display the inline position reminder again until the user logs out or loses the session cookie
       
if( $link_position == 'inline' )
        {
           
$Session->set( 'display_inline_reminder', 'false' );
        }

       
// Check permission:
       
$LinkOwner->check_perm( 'edit', true );

       
$edited_Link->set( 'position', $link_position ); // This returns false if no change was made
       
if( $edited_Link->dbupdate() !== false ) // This returns NULL if no change was made
       
{ // update was successful or no change was made

            // Update last touched date of Owners
           
$LinkOwner->update_last_touched_date();

            if(
$LinkOwner->type == 'item' &&
                (
$link_position == 'cover' || $link_position == 'background' ) )
            {
// Position "Cover" can be used only by one link
              // Replace previous position with "Inline"
               
$DB->query( 'UPDATE T_links
                        SET link_position = "aftermore"
                    WHERE link_ID != '
.$DB->quote( $link_ID ).'
                        AND link_itm_ID = '
.$DB->quote( $LinkOwner->Item->ID ).'
                        AND link_position = "cover"'
);
            }

           
$this->halt( 'Link position has been updated.', 'change_position_success', 200 );
           
// Exit here
       
}
        else
        {
// return the current value on failure
           
$this->halt( 'Failed to change link position.', 'change_position_failed', 403 );
           
// Exit here
       
}
    }


   
/**
     * Call link controller to change order the requested link
     */
   
private function controller_link_change_order()
    {
        global
$localtimenow;

       
// Check permission if current user can update the requested link:
       
$this->link_check_perm();

       
// Get action from request string:
       
$link_action = $this->args[2];

       
$edited_Link = & $this->get_Link();
       
$LinkOwner = & $edited_Link->get_LinkOwner();

       
$ownerLinks = $LinkOwner->get_Links();

       
// TODO fp> when moving an "after_more" above a "teaser" img, it should change to "teaser" too.
        // TODO fp> when moving a "teaser" below an "aftermore" img, it should change to "aftermore" too.

        // Switch order with the next/prev one:
       
if( $link_action == 'move_up' )
        {
           
$switchcond = 'return ($loop_Link->get("order") > $i
                && $loop_Link->get("order") < '
.$edited_Link->get( 'order' ).');';
           
$i = -1;
        }
        else
        {
           
$switchcond = 'return ($loop_Link->get("order") < $i
                && $loop_Link->get("order") > '
.$edited_Link->get( 'order' ).');';
           
$i = PHP_INT_MAX;
        }
        foreach(
$ownerLinks as $loop_Link )
        {    
// Find nearest order:
           
if( $loop_Link == $edited_Link )
                continue;

            if( eval(
$switchcond ) )
            {
               
$i = $loop_Link->get( 'order' );
               
$switch_Link = $loop_Link;
            }
        }
        if(
$i > -1 && $i < PHP_INT_MAX )
        {    
// Switch the links:
           
if( $LinkOwner->type == 'item' && ( $localtimenow - strtotime( $LinkOwner->Item->last_touched_ts ) ) > 90 )
            {
               
$LinkOwner->Item->create_revision();
            }

           
// Update last touched date of Owners
            // Update to last touched date made earlier to prevent subsequent link dbupdate from creating a new revision
           
$LinkOwner->update_last_touched_date();

           
$switch_Link->set( 'order', $edited_Link->get( 'order' ) );

           
// HACK: go through order=0 to avoid duplicate key conflict:
           
$edited_Link->set( 'order', 0 );
           
$edited_Link->dbupdate();
           
$switch_Link->dbupdate();

           
$edited_Link->set( 'order', $i );
           
$edited_Link->dbupdate();

           
// The requested link order has been changed successfully:
           
$this->halt( ( $link_action == 'move_up' )
                ?
T_('Link has been moved up.')
                :
T_('Link has been moved down.'),
               
'change_order_success', 200 );
           
// Exit here.
       
}
        else
        {    
// The requested link order has been changed successfully:
           
$this->halt( T_('Link order has not been changed.'), 'change_order_success', 403 );
           
// Exit here.
       
}
    }


   
/**
     * Call link controller to change order the requested link
     */
   
private function controller_link_attach()
    {
        global
$LinkOwner, $current_File;

       
$link_type = param( 'type', 'string' );
       
$link_object_ID = param( 'object_ID', 'string' );
       
$root = param( 'root', 'string' );
       
$file_path = param( 'path', 'string' );

       
$LinkOwner = get_LinkOwner( $link_type, $link_object_ID );

        if( !
$LinkOwner->check_perm( 'edit', false ) )
        {    
// Current user has no permission to unlink the requested link:
           
$this->halt( 'You have no permission to attach a file!', 'no_access', 403 );
           
// Exit here.
       
}

       
$FileCache = & get_FileCache();
        list(
$root_type, $root_in_type_ID ) = explode( '_', $root, 2 );
        if( ! (
$current_File = $FileCache->get_by_root_and_path( $root_type, $root_in_type_ID, $file_path ) ) )
        {    
// No file:
           
$this->halt( T_('Nothing selected.'), 'wrong_file', 403 );
           
// Exit here.
       
}

       
// Link the file to Item/Comment:
       
$link_ID = $current_File->link_to_Object( $LinkOwner );

       
$LinkCache = & get_LinkCache();
        if(
$Link = & $LinkCache->get_by_ID( $link_ID, false, false ) )
        {    
// Add data of new link to response:

            // Use the glyph or font-awesome icons if requested by skin
           
param( 'b2evo_icons_type', 'string', 'fontawesome-glyphicons' );

            global
$disable_evo_flush;

           
$LinkOwner = get_LinkOwner( $link_type, $link_object_ID );

           
// Initialize admin skin:
           
global $current_User, $UserSettings, $is_admin_page, $adminskins_path, $AdminUI;
           
$admin_skin = $UserSettings->get( 'admin_skin', $current_User->ID );
           
$is_admin_page = true;
            require_once
$adminskins_path.$admin_skin.'/_adminUI.class.php';
           
$AdminUI = new AdminUI();

           
// Disable function evo_flush() to correct handle a content below:
           
$disable_evo_flush = true;

           
// Get the refreshed content:
           
ob_start();
           
$AdminUI->disp_view( 'links/views/_link_list.view.php' );
           
$refreshed_content = ob_get_clean();

           
$mask_row = (object) array(
                   
'link_ID'       => $Link->ID,
                   
'file_ID'       => $current_File->ID,
                   
'file_type'     => $current_File->get_file_type(),
                   
'link_position' => $Link->get( 'position' ),
                );
           
$this->add_response( 'link', array(
                   
'ID'       => $Link->ID,
                   
'url'      => $current_File->get_view_link(),
                   
'preview'  => $Link->get_preview_thumb(),
                   
'actions'  => link_actions( $Link->ID, 'last', $link_type ),
                   
'position' => display_link_position( $mask_row ),
                ) );
           
$this->add_response( 'list_content', $refreshed_content );
        }

       
// File has been attached successfully:
       
$this->halt( $LinkOwner->translate( 'Selected files have been linked to xxx.' ), 'attach_success', 200 );
       
// Exit here.
   
}


   
/**
     * Call link controller to refresh a list of the links
     */
   
private function controller_link_refresh()
    {
        global
$LinkOwner, $current_File, $disable_evo_flush;

       
$link_type = param( 'type', 'string' );
       
$link_object_ID = param( 'object_ID', 'string' );
       
$fieldset_prefix = param( 'prefix', 'string', NULL );

       
$LinkOwner = get_LinkOwner( $link_type, $link_object_ID );

        if( !
$LinkOwner->check_perm( 'view', false ) )
        {    
// Current user has no permission to unlink the requested link:
           
$this->halt( 'You have no permission to list of the links!', 'no_access', 403 );
           
// Exit here.
       
}

        if(
get_param( 'action' ) == 'sort' )
        {    
// Sort the links:
           
$ownerLinks = $LinkOwner->get_Links();
           
usort( $ownerLinks, 'sort_links_by_filename' );

           
$max_order = 0;
           
$link_orders = array();
           
$link_count = count( $ownerLinks );
            foreach(
$ownerLinks as $link )
            {
                if(
$link->order > $max_order )
                {
                   
$max_order = $link->order;
                }
               
$link_orders[] = $link->order;
            }

            for(
$i = 1; $i <= $link_count; $i++ )
            {
                   
$ownerLinks[$i - 1]->set( 'order', $i + $max_order );
                   
$ownerLinks[$i - 1]->dbupdate();
            }

            for(
$i = 1; $i <= $link_count; $i++ )
            {
                if(
$ownerLinks[$i -1]->get( 'order' ) != $i )
                {
                   
$ownerLinks[$i - 1]->set( 'order', $i );
                   
$ownerLinks[$i - 1]->dbupdate();
                }
            }
        }

       
// Initialize admin skin:
       
global $current_User, $UserSettings, $is_admin_page, $adminskins_path, $AdminUI, $inc_path;
       
$admin_skin = is_logged_in() ? $UserSettings->get( 'admin_skin', $current_User->ID ) : 'bootstrap';
       
$is_admin_page = true;
        require_once
$adminskins_path.$admin_skin.'/_adminUI.class.php';
       
$AdminUI = new AdminUI();

       
// Disable function evo_flush() to correct handle a content below:
       
$disable_evo_flush = true;

       
// Get the refreshed content:
       
ob_start();
       
// We do not use $AdminUI->disp_view() because we need the $fieldset_prefix above:
       
require $inc_path.'links/views/_link_list.view.php';
       
$refreshed_content = ob_get_clean();

       
$this->add_response( 'html', $refreshed_content );

        if(
get_param( 'action' ) == 'sort' )
        {    
// The sort has been done successfully:
           
$this->halt( T_('The attachments have been sorted by file name.'), 'sort_success', 200 );
           
// Exit here.
       
}
        else
        {    
// The refresh has been done successfully:
           
$this->halt( 'A list of the links has been refreshed.', 'refresh_success', 200 );
           
// Exit here.
       
}
    }


   
/**
     * Call link controller to copy Link from one object to another
     */
   
private function controller_link_copy()
    {
       
$dest_type = param( 'dest_type', 'string' );
       
$dest_object_ID = param( 'dest_object_ID', 'string' );

       
$dest_LinkOwner = get_LinkOwner( $dest_type, $dest_object_ID );

        if( !
$dest_LinkOwner->check_perm( 'edit', false ) )
        {    
// Current user has no permission to copy the requested link:
           
$this->halt( 'You have no permission to list of the links!', 'no_access', 403 );
           
// Exit here.
       
}

       
$source_type = param( 'source_type', 'string' );
       
$source_object_ID = param( 'source_object_ID', 'string' );
       
$source_position = trim( param( 'source_position', 'string' ), ',' );
       
$source_file_type = param( 'source_file_type', 'string', NULL );

       
$source_LinkOwner = get_LinkOwner( $source_type, $source_object_ID );

       
$link_list_params = array(
               
// Sort the attachments to get firstly "Cover", then "Teaser", and "After more" as last order
               
'sql_select_add' => ', CASE '
                       
.'WHEN link_position = "cover"     THEN "1" '
                       
.'WHEN link_position = "teaser"    THEN "2" '
                       
.'WHEN link_position = "aftermore" THEN "3" '
                       
.'WHEN link_position = "inline"    THEN "4" '
                       
.'ELSE "99999999"' // Use this line only if you want to put the other position types at the end
                   
.'END AS position_order',
               
'sql_order_by' => 'position_order, link_order',
            );

        if( !
$source_LinkOwner || ! ( $source_LinkList = $source_LinkOwner->get_attachment_LinkList( 1000, $source_position, $source_file_type, $link_list_params ) ) )
        {    
// No requested links, Exit here:
           
$this->halt( 'No requested links!', 'no_links', 404 );
           
// Exit here.
       
}

       
$dest_position = param( 'dest_position', 'string' );

       
$dest_last_order = $dest_LinkOwner->get_last_order() + 1;

       
// Limit files per each position, 0 - for unlimit:
       
$limit_position = param( 'limit_position', 'integer', 0 );
        if(
$limit_position )
        {
           
$position_counts = array();
           
$all_position_counts = 0;
        }

       
// Find all attached files of the destination LinkOwner in order to don't link twice same files:
       
$dest_files_links = array();
       
$dest_links = $dest_LinkOwner->get_Links();
        if( ! empty(
$dest_links ) && is_array( $dest_links ) )
        {
            foreach(
$dest_links as $dest_Link )
            {
                if(
$dest_File = & $dest_Link->get_File() )
                {
                   
$dest_files_links[ $dest_File->ID ] = $dest_Link->ID;
                }
            }
        }

        while(
$source_Link = & $source_LinkList->get_next() )
        {    
// Copy a Link to new object:
           
if( $limit_position )
            {
                if(
$source_position == '' && $all_position_counts >= $limit_position )
                {    
// Stop copy other positions because we are finding any position:
                   
break;
                }
                if( ! isset(
$position_counts[ $source_Link->position ] ) )
                {
                   
$position_counts[ $source_Link->position ] = 0;
                }
                if(
$position_counts[ $source_Link->position ] >= $limit_position )
                {    
// Skip this because of limit per position:
                   
if( $source_position == $source_Link->position )
                    {    
// Stop copy other positions because we are finding only current:
                       
break;
                    }
                    else
                    {    
// Continue to find other positions:
                       
continue;
                    }
                }
               
$position_counts[ $source_Link->position ]++;
               
$all_position_counts++;
            }

            if(
$source_File = & $source_Link->get_File() )
            {
                if( isset(
$dest_files_links[ $source_File->ID ] ) )
                {    
// Don't attach the File twice if it is already linked to the destination LinkOwner:
                   
$new_link_ID = $dest_files_links[ $source_File->ID ];
                }
                else
                {    
// Attach new file because it is not linked to the destination LinkOwner yet:
                   
$new_link_ID = $dest_LinkOwner->add_link( $source_Link->file_ID, ( empty( $dest_position ) ? $source_Link->position : $dest_position ), $dest_last_order++ );
                }
               
$this->add_response( 'links', array(
                       
'ID'            => $new_link_ID,
                       
'file_type'     => $source_File->get_file_type(),
                       
'orig_position' => $source_Link->position,
                    ),
'array' );
            }
        }
    }


   
/**** MODULE LINKS ---- END ****/

    /**** MODULE TOOLS ---- START ****/

    /**
     * Call module to prepare request for tools
     */
   
private function module_tools()
    {
        if( empty(
$this->args[1] ) )
        {
           
$this->halt( 'Missing tool controller', 'wrong_request', 405 );
        }
        else
        {
           
$tool_controller = $this->args[1];

            if( !
method_exists( $this, 'controller_tool_'.$tool_controller ) )
            {    
// Unknown controller:
               
$this->halt( 'Unknown link controller "'.$tool_controller.'"', 'unknown_controller' );
               
// Exit here.
           
}

           
// Call collection controller to prepare current request:
           
$this->{'controller_tool_'.$tool_controller}();
        }
    }


   
/**
     * Call tool controller to get available urlname
     */
   
private function controller_tool_available_urlname()
    {
       
$urlname = param( 'urlname', 'string' );

       
$this->add_response( 'base', $urlname );
       
$this->add_response( 'urlname', urltitle_validate( $urlname, '', 0, false, 'blog_urlname', 'blog_ID', 'T_blogs' ) );
    }

   
/**** MODULE TOOLS ---- END ****/
}