web/wp-content/plugins/twitter-tools/classes/aktt.php
changeset 194 32102edaa81b
equal deleted inserted replaced
193:2f6f6f7551ca 194:32102edaa81b
       
     1 <?php
       
     2 
       
     3 class AKTT {
       
     4 	// settings: aktt_v3_settings
       
     5 	static $ver = '3.0.1';
       
     6 	static $enabled = false;
       
     7 	static $prefix = 'aktt_';
       
     8 	static $post_type = 'aktt_tweet';
       
     9 	static $text_domain = 'twitter-tools';
       
    10 	static $menu_page_slug = 'twitter-tools';
       
    11 	static $plugin_settings_section_slug = 'aktt_plugin_settings_group';
       
    12 	static $account_settings_section_slug = 'aktt_account_settings';
       
    13 	static $cap_options = 'manage_options';
       
    14 	static $cap_download = 'publish_posts';
       
    15 	static $admin_notices = array();
       
    16 	static $settings = array();
       
    17 	static $accounts = array();
       
    18 	static $debug = false;
       
    19 	
       
    20 	/**
       
    21 	 * Sets whether or not the plugin should be enabled.  Also initialize the plugin's settings.
       
    22 	 *
       
    23 	 * @return void
       
    24 	 */
       
    25 	static function after_setup_theme() {
       
    26 		self::add_thumbnail_support();
       
    27 	}
       
    28 	
       
    29 	static function add_thumbnail_support() {
       
    30 		$thumbnails = get_theme_support('post-thumbnails');
       
    31 		if (is_array($thumbnails)) {
       
    32 			add_theme_support('post-thumbnails', array_merge($thumbnails[0], array(self::$post_type)));
       
    33 		}
       
    34 		else if (!$thumbnails) {
       
    35 			add_theme_support('post-thumbnails', array(self::$post_type));
       
    36 		}
       
    37 		// else already enabled for all post types
       
    38 	}
       
    39 
       
    40 	/**
       
    41 	 * Sets whether or not the plugin should be enabled.  Also initialize the plugin's settings.
       
    42 	 *
       
    43 	 * @return void
       
    44 	 */
       
    45 	static function init() {
       
    46 		add_action('admin_notices', array('AKTT', 'admin_notices'));
       
    47 
       
    48 		self::$enabled = class_exists('Social');
       
    49 		if (!self::$enabled) {
       
    50 			self::add_admin_notice(sprintf(__('Twitter Tools relies on the <a href="%s">Social plugin</a>, please install this plugin.', 'twitter-tools'), 'http://wordpress.org/extend/plugins/social/'), 'error');
       
    51 			return;
       
    52 		}
       
    53 		
       
    54 		/* Set our default settings.  We need to do this at init() so 
       
    55 		that any text domains (i18n) are registered prior to us setting 
       
    56 		the labels. */
       
    57 		self::set_default_settings();
       
    58 		
       
    59 		self::register_post_type();
       
    60 		self::register_taxonomies();
       
    61 
       
    62 		add_shortcode('aktt_tweets', 'aktt_shortcode_tweets');
       
    63 		add_shortcode('aktt_tweet', 'aktt_shortcode_tweet');
       
    64 
       
    65 		// General Hooks
       
    66 		add_action('wp', array('AKTT', 'controller'), 1);
       
    67 		add_filter('the_post', array('AKTT', 'the_post'));
       
    68 		add_filter('the_posts', array('AKTT', 'the_posts'));
       
    69 		add_action('social_account_disconnected', array('AKTT', 'social_account_disconnected'), 10, 2);
       
    70 		add_action('social_broadcast_response', array('AKTT', 'social_broadcast_response'), 10, 3);
       
    71 		
       
    72 		// Admin Hooks
       
    73 		add_action('admin_init', array('AKTT', 'init_settings'), 0);
       
    74 		add_action('admin_init', array('AKTT', 'admin_controller'), 1);
       
    75 		add_action('admin_menu', array('AKTT', 'admin_menu'));
       
    76 		add_filter('plugin_action_links', array('AKTT', 'plugin_action_links'), 10, 2);
       
    77 		add_action('admin_enqueue_scripts', array('AKTT', 'admin_enqueue_scripts'));
       
    78 		
       
    79 		// Cron Hooks
       
    80 		add_action('social_cron_15', array('AKTT', 'import_tweets'));
       
    81 		add_action('aktt_backfill_tweets', array('AKTT', 'backfill_tweets'));
       
    82 		
       
    83 		// Set logging to admin screen settings
       
    84 		self::$debug = self::option('debug');
       
    85 	}
       
    86 	
       
    87 	
       
    88 	/**
       
    89 	 * Sets the default settings for the plugin
       
    90 	 *
       
    91 	 * @return void
       
    92 	 */
       
    93 	static function set_default_settings() {
       
    94 		// Set default settings
       
    95 		$yn_options = array(
       
    96 			'1' => __('Yes', 'twitter-tools'),
       
    97 			'0' => __('No', 'twitter-tools')
       
    98 		);
       
    99 		$settings = array(
       
   100 			'tweet_admin_ui' => array(
       
   101 				'name' => 'tweet_admin_ui',
       
   102 				'value' => 1,
       
   103 				'label' => __('Show admin screens for tweets', 'twitter-tools'),
       
   104 				'type' => 'radio',
       
   105 				'options' => $yn_options,
       
   106 			),
       
   107 			'tweet_visibility' => array(
       
   108 				'name' => 'tweet_visibility',
       
   109 				'value' => 1,
       
   110 				'label' => __('Create URLs for tweets', 'twitter-tools'),
       
   111 				'type' => 'radio',
       
   112 				'options' => array(
       
   113 					'1' => sprintf(__('Yes <span class="help">(%s)</span>', 'twitter-tools'), home_url('tweet/{tweet-id}')),
       
   114 					'0' => __('No', 'twitter-tools')
       
   115 				),
       
   116 			),
       
   117 			'credit' => array(
       
   118 				'name' => 'credit',
       
   119 				'value' => 1,
       
   120 				'label' => __('Give Twitter Tools credit', 'twitter-tools'),
       
   121 				'type' => 'radio',
       
   122 				'options' => $yn_options,
       
   123 			),
       
   124 			'debug' => array(
       
   125 				'name' => 'debug',
       
   126 				'value' => 0,
       
   127 				'label' => __('Debug logging', 'twitter-tools'),
       
   128 				'type' => 'radio',
       
   129 				'options' => array(
       
   130 					'0' => __('Disabled', 'twitter-tools'),
       
   131 					'1' => __('Enabled <span class="help">(written to the PHP error log)</span>', 'twitter-tools'),
       
   132 				),
       
   133 			),
       
   134 		);
       
   135 		self::$settings = apply_filters('aktt_default_settings', $settings);
       
   136 	}
       
   137 	
       
   138 	
       
   139 	/**
       
   140 	 * Append a message of a certain type to the admin notices.
       
   141 	 *
       
   142 	 * @param string $msg 
       
   143 	 * @param string $type 
       
   144 	 * @return void
       
   145 	 */
       
   146 	static function add_admin_notice($msg, $type = 'updated') {
       
   147 		self::$admin_notices[] = array(
       
   148 			'type' => $type == 'error' ? $type : 'updated', // If it's not an error, set it to updated
       
   149 			'msg' => $msg
       
   150 		);
       
   151 	}
       
   152 	
       
   153 	
       
   154 	/**
       
   155 	 * Displays admin notices 
       
   156 	 *
       
   157 	 * @return void
       
   158 	 */
       
   159 	static function admin_notices() {
       
   160 		if (is_array(self::$admin_notices)) {
       
   161 			foreach (self::$admin_notices as $notice) {
       
   162 				extract($notice);
       
   163 				?>
       
   164 				<div class="<?php echo esc_attr($type); ?>">
       
   165 					<p><?php echo $msg; ?></p>
       
   166 				</div><!-- /<?php echo esc_html($type); ?> -->
       
   167 				<?php
       
   168 			}
       
   169 		}
       
   170 	}
       
   171 	
       
   172 	
       
   173 	/**
       
   174 	 * Registers the aktt_tweet post type
       
   175 	 *
       
   176 	 * @return void
       
   177 	 */
       
   178 	static function register_post_type() {
       
   179 		register_post_type(self::$post_type, array(
       
   180 			'labels' => array(
       
   181 				'name' => __('Tweets', 'twitter-tools'),
       
   182 				'singular_name' => __('Tweet', 'twitter-tools')
       
   183 			),
       
   184 			'supports' => array(
       
   185 				'editor',
       
   186 				'thumbnail',
       
   187 			),
       
   188 			'public' => (bool) self::option('tweet_visibility'),
       
   189 			'show_ui' => (bool) self::option('tweet_admin_ui'),
       
   190 			'rewrite' => array(
       
   191 				'slug' => 'tweets',
       
   192 				'with_front' => false
       
   193 			),
       
   194 			'has_archive' => true,
       
   195 		));
       
   196 	}
       
   197 	
       
   198 	
       
   199 	/**
       
   200 	 * Register our taxonomies.
       
   201 	 * 
       
   202 	 * @return void
       
   203 	 */
       
   204 	static function register_taxonomies() {
       
   205 		$defaults = array(
       
   206 			'public' => (bool) self::option('tweet_visibility'),
       
   207 			'show_ui' => (bool) self::option('tweet_admin_ui'),
       
   208 		);
       
   209 		$taxonomies = array(
       
   210 			'aktt_accounts' => array_merge($defaults, array(
       
   211 				'labels' => array(
       
   212 					'name' => __('Accounts', 'twitter-tools'),
       
   213 					'singular_name' => __('Account', 'twitter-tools')
       
   214 				),
       
   215 				'rewrite' => array(
       
   216 					'slug' => 'tweet-accounts',
       
   217 					'with_front' => false
       
   218 				),
       
   219 			)),
       
   220 			'aktt_mentions' => array_merge($defaults, array(
       
   221 				'labels' => array(
       
   222 					'name' => __('Mentions', 'twitter-tools'),
       
   223 					'singular_name' => __('Mention', 'twitter-tools')
       
   224 				),
       
   225 				'rewrite' => array(
       
   226 					'slug' => 'tweet-mentions',
       
   227 					'with_front' => false
       
   228 				),
       
   229 			)),
       
   230 			'aktt_hashtags' => array_merge($defaults, array(
       
   231 				'labels' => array(
       
   232 					'name' => __('Hashtags', 'twitter-tools'),
       
   233 					'singular_name' => __('Hashtag', 'twitter-tools')
       
   234 				),
       
   235 				'rewrite' => array(
       
   236 					'slug' => 'tweet-hashtags',
       
   237 					'with_front' => false
       
   238 				),
       
   239 			)),
       
   240 			'aktt_types' => array_merge($defaults, array(
       
   241 				'labels' => array(
       
   242 					'name' => __('Types', 'twitter-tools'),
       
   243 					'singular_name' => __('Type', 'twitter-tools')
       
   244 				),
       
   245 				'rewrite' => array(
       
   246 					'slug' => 'tweet-types',
       
   247 					'with_front' => false
       
   248 				),
       
   249 				'public' => false,
       
   250 				'show_ui' => false,
       
   251 			)),
       
   252 		);
       
   253 		foreach ($taxonomies as $tax => $args) {
       
   254 			register_taxonomy($tax, self::$post_type, $args);
       
   255 		}
       
   256 	}
       
   257 	
       
   258 	
       
   259 	/**
       
   260 	 * Get or update an option from the DB, and fall back to the default setting
       
   261 	 *
       
   262 	 * @param string $setting 
       
   263 	 * @return mixed
       
   264 	 */
       
   265 	static function option($key, $value = null) {
       
   266 		// Do we have an option?
       
   267 		$option = get_option('aktt_v3_settings');
       
   268 		if (!is_null($value)) {
       
   269 			$option[$key] = $value;
       
   270 			return update_option('aktt_v3_settings', $option);
       
   271 		}
       
   272 		if (!empty($option) && is_array($option) && isset($option[$key])) {
       
   273 			$val = $option[$key];
       
   274 		}
       
   275 		else { // Get a default
       
   276 			$val = isset(self::$settings[$key]) ? self::$settings[$key]['value'] : null;
       
   277 		}
       
   278 		return apply_filters('aktt_get_option', $val, $key);
       
   279 	}
       
   280 	
       
   281 	
       
   282 	/**
       
   283 	 * Utility function to get tweets (used by shortcode, widget, etc.)
       
   284 	 *
       
   285 	 * @param array $args 
       
   286 	 * @return array
       
   287 	 */
       
   288 	static function get_tweets($args) {
       
   289 		$defaults = array(
       
   290 			'account' => array(),
       
   291 			'id' => null,
       
   292 			'count' => 5,
       
   293 			'offset' => 0,
       
   294 			'mentions' => array(),
       
   295 			'hashtags' => array(),
       
   296 			'include_rts' => 0,
       
   297 			'include_replies' => 0,
       
   298 		);
       
   299 		$taxonomies = array(
       
   300 			'aktt_accounts' => array(
       
   301 				'var' => 'account',
       
   302 				'strip' => array()
       
   303 			),
       
   304 			'aktt_hashtags' => array(
       
   305 				'var' => 'hashtags',
       
   306 				'strip' => array('#')
       
   307 			),
       
   308 			'aktt_mentions' => array(
       
   309 				'var' => 'mentions',
       
   310 				'strip' => array('@')
       
   311 			)
       
   312 		);
       
   313 		foreach ($taxonomies as $data) {
       
   314 			$tax = $data['var'];
       
   315 			$strip = $data['strip'];
       
   316 			if (isset($args[$tax])) {
       
   317 				$terms = array();
       
   318 				foreach(explode(',', $args[$tax]) as $term) {
       
   319 					$term = trim(str_replace($strip, '', $term));
       
   320 					if (!empty($term)) {
       
   321 						$terms[] = $term;
       
   322 					}
       
   323 				}
       
   324 				$args[$tax] = $terms;
       
   325 			}
       
   326 		}
       
   327 		$params = array_merge($defaults, $args);
       
   328 		$query_data = array(
       
   329 			'post_type' => 'aktt_tweet',
       
   330 			'posts_per_page' => $params['count'],
       
   331 			'offset' => $params['offset'],
       
   332 		);
       
   333 // set tweet ID
       
   334 		if (!empty($params['id'])) {
       
   335 			$query_data['meta_query'] = array(array(
       
   336 				'key' => '_aktt_tweet_id',
       
   337 				'value' => $params['id'],
       
   338 				'compare' => '='
       
   339 			));
       
   340 		}
       
   341 		else {
       
   342 // process tax data
       
   343 			$tax_query = array(
       
   344 				'relation' => 'AND'
       
   345 			);
       
   346 // set accounts, mentions, hashtags
       
   347 			foreach ($taxonomies as $tax => $data) {
       
   348 				$var = $data['var'];
       
   349 				if (isset($params[$var]) && count($params[$var])) {
       
   350 					$query = array(
       
   351 						'taxonomy' => $tax,
       
   352 						'field' => 'slug',
       
   353 						'terms' => array()
       
   354 					);
       
   355 					foreach ($params[$var] as $term) {
       
   356 						$query['terms'][] = $term;
       
   357 					}
       
   358 					$tax_query[] = $query;
       
   359 				}
       
   360 			}
       
   361 // always hide broadcasts - can be overridden with filter below
       
   362 			$type_terms = array(
       
   363 				'social-broadcast'
       
   364 			);
       
   365 // other exclusions - this is a NOT IN query
       
   366 			if (!$params['include_rts']) {
       
   367 				$type_terms[] = 'retweet';
       
   368 			}
       
   369 			if (!$params['include_replies']) {
       
   370 				$type_terms[] = 'reply';
       
   371 			}
       
   372 			$tax_query[] = array(
       
   373 				'taxonomy' => 'aktt_types',
       
   374 				'field' => 'slug',
       
   375 				'terms' => $type_terms,
       
   376 				'operator' => 'NOT IN'
       
   377 			);
       
   378 			$query_data['tax_query'] = $tax_query;
       
   379 		}
       
   380 		$query = new WP_Query(apply_filters('aktt_get_tweets', $query_data));
       
   381 		return $query->posts;
       
   382 	}
       
   383 	
       
   384 	/**
       
   385 	 * Attach tweet data to post and replace entities in the post content
       
   386 	 *
       
   387 	 * @param stdClass $post
       
   388 	 * @return stdClass
       
   389 	 */
       
   390 	static function the_post($post) {
       
   391 		if ($post->post_type == self::$post_type && empty($post->tweet)) {
       
   392 			if ($raw_data = get_post_meta($post->ID, '_aktt_tweet_raw_data', true)) {
       
   393 				$post->tweet = new AKTT_Tweet(json_decode($raw_data));
       
   394 				$post->post_content = $post->tweet->link_entities();
       
   395 			}
       
   396 			if (has_post_thumbnail($post->ID)) {
       
   397 				$size = apply_filters('aktt_featured_image_size', 'medium');
       
   398 				$post->post_content .= "\n\n".get_the_post_thumbnail(null, $size);
       
   399 			}
       
   400 		}
       
   401 		return $post;
       
   402 	}
       
   403 	
       
   404 	/**
       
   405 	 * Attach tweet data to posts
       
   406 	 *
       
   407 	 * @param array $posts
       
   408 	 * @return array
       
   409 	 */
       
   410 	static function the_posts($posts) {
       
   411 		foreach ($posts as &$post) {
       
   412 			AKTT::the_post($post);
       
   413 		}
       
   414 		return $posts;
       
   415 	}
       
   416 	
       
   417 	/**
       
   418 	 * Prepends a "settings" link for our plugin on the plugins.php page
       
   419 	 *
       
   420 	 * @param array $links 
       
   421 	 * @param string $file -- filename of plugin 
       
   422 	 * @return array
       
   423 	 */
       
   424 	function plugin_action_links($links, $file) {
       
   425 		if (basename($file) == basename(AKTT_FILE)) {
       
   426 			$settings_link = '<a href="options-general.php?page='.self::$menu_page_slug.'">'.__('Settings', 'twitter-tools').'</a>';
       
   427 			array_unshift($links, $settings_link);
       
   428 		}
       
   429 		return $links;
       
   430 	}
       
   431 	
       
   432 	
       
   433 	/**
       
   434 	 * Adds a link to the "Settings" menu in WP-Admin.
       
   435 	 */
       
   436 	public function admin_menu() {
       
   437 		add_options_page(
       
   438 			__('Twitter Tools Options', 'twitter-tools'),
       
   439 			__('Twitter Tools', 'twitter-tools'),
       
   440 			self::$cap_options,
       
   441 			self::$menu_page_slug,
       
   442 			array('AKTT', 'settings_page')
       
   443 		);
       
   444 	}
       
   445 	
       
   446 	static function maybe_create_db_index($col, $key_name = null, $table_name = null) {
       
   447 		global $wpdb;
       
   448 		if (empty($key_name)) {
       
   449 			$key_name = $col;
       
   450 		}
       
   451 		if (empty($table_name)) {
       
   452 			$table_name = $wpdb->posts;
       
   453 		}
       
   454 		// Add a GUID index if none exists
       
   455 		$results = $wpdb->get_results($wpdb->prepare("
       
   456 			SHOW INDEX
       
   457 			FROM $table_name
       
   458 			WHERE KEY_NAME = '%s'
       
   459 		", $key_name));
       
   460 		if (!count($results)) {
       
   461 			$wpdb->query("
       
   462 				ALTER TABLE $table_name
       
   463 				ADD INDEX ($col)
       
   464 			"); // can's use $wpdb->prepare here
       
   465 		}
       
   466 	}
       
   467 	
       
   468 	/**
       
   469 	 * Initializes the plugin settings in WP admin, using the Settings API
       
   470 	 *
       
   471 	 * @return void
       
   472 	 */
       
   473 	static function init_settings() {
       
   474 		
       
   475 		// Register our parent setting (it contains an array of all our plugin-wide settings)
       
   476 		register_setting(
       
   477 			self::$menu_page_slug, // Page it belongs to
       
   478 			'aktt_v3_settings', // option name
       
   479 			array('AKTT', 'sanitize_plugin_settings') // Sanitize callback
       
   480 		);
       
   481 		
       
   482 		// Register our account settings
       
   483 		register_setting(
       
   484 			self::$menu_page_slug, // Page it belongs to
       
   485 			'aktt_v3_accounts', // option name
       
   486 			array('AKTT', 'sanitize_account_settings') // Sanitize callback
       
   487 		);
       
   488 		
       
   489 	}
       
   490 	
       
   491 	
       
   492 	/**
       
   493 	 * Sanitization of values
       
   494 	 *
       
   495 	 * @param mixed $value 
       
   496 	 * @return int
       
   497 	 */
       
   498 	static function sanitize_plugin_settings($value) {
       
   499 		self::maybe_create_db_index('guid');
       
   500 		flush_rewrite_rules(false);
       
   501 		if (is_array($value)) {
       
   502 			foreach ($value as $k => $v) {
       
   503 				$value[$k] = self::sanitize_plugin_setting($k, $v);
       
   504 			}
       
   505 		}
       
   506 		return $value;
       
   507 	}
       
   508 	
       
   509 	/**
       
   510 	 * Sanitizes the ACCOUNT settings from the Twitter Tools' admin page.
       
   511 	 * 
       
   512 	 *	** Option Storage Format **
       
   513 	 *	
       
   514 	 *	$option_value = array(
       
   515 	 *		$this->id => array(
       
   516 	 *			'settings' => array(
       
   517 	 *				'post_author' => 0,
       
   518 	 *				'post_cats' => array(),
       
   519 	 *				'post_tags' => array(),
       
   520 	 *				'hashtag' => '',
       
   521 	 *			),
       
   522 	 *		),
       
   523 	 *	);
       
   524 	 *
       
   525 	 * @param array $value 
       
   526 	 * @return array
       
   527 	 */
       
   528 	static function sanitize_account_settings($value) {
       
   529 		if (is_array($value)) {
       
   530 			foreach ($value as $id => &$acct) {
       
   531 				// If we don't have a settings array, get rid of it
       
   532 				if (!isset($acct['settings'])) {
       
   533 					unset($value[$id]);
       
   534 					continue;
       
   535 				}
       
   536 				
       
   537 				// Loop over each setting and sanitize
       
   538 				foreach (array_keys(AKTT_Account::$settings) as $key) {
       
   539 					if (!isset($acct['settings'][$key])) {
       
   540 						$acct['settings'][$key] = null;
       
   541 					}
       
   542 					$acct['settings'][$key] = self::sanitize_account_setting($key, $acct['settings'][$key]);
       
   543 				}
       
   544 			}
       
   545 		}
       
   546 		else {
       
   547 			$value = null;
       
   548 		}
       
   549 		return $value;
       
   550 	}
       
   551 	
       
   552 	
       
   553 	static function sanitize_plugin_setting($key, $value) {
       
   554 		return self::sanitize_setting($key, $value, self::$settings[$key]['type']);
       
   555 	}
       
   556 	
       
   557 	static function sanitize_account_setting($key, $value) {
       
   558 		return self::sanitize_setting($key, $value, AKTT_Account::$settings[$key]['type']);
       
   559 	}
       
   560 	
       
   561 	
       
   562 	/**
       
   563 	 * Sanitizes a setting, based on a big switch statement 
       
   564 	 * that has each setting, and how to clean it.
       
   565 	 *
       
   566 	 * @param string $key 
       
   567 	 * @param mixed $value 
       
   568 	 * @param string $type - type of setting (int, etc.)
       
   569 	 * @return mixed - Clean value **If it matched a switch case**
       
   570 	 */
       
   571 	static function sanitize_setting($key, $value, $type) {
       
   572 		switch ($type) {
       
   573 			case 'int':
       
   574 				$value = is_array($value) ? array_map('intval', $value) : intval($value);
       
   575 				break;
       
   576 			case 'no_html':
       
   577 				$value = is_array($value) ? array_map('wp_filter_nohtml_kses', $value) : wp_filter_nohtml_kses($value);
       
   578 				break;
       
   579 			case 'tags':
       
   580 				$value = trim($value);
       
   581 				if (!empty($value)) {
       
   582 					$tags_clean = array();
       
   583 					$tags_input = array_map('trim', explode(',', $value));
       
   584 					foreach ($tags_input as $tag) {
       
   585 						if (!empty($tag)) {
       
   586 							$tags_clean[] = $tag;
       
   587 							if (!get_term_by('name', $tag, 'post_tag')) {
       
   588 								wp_insert_term($tag, 'post_tag');
       
   589 							}
       
   590 						}
       
   591 					}
       
   592 					unset($tags_input);
       
   593 					$value = implode(', ', $tags_clean);
       
   594 				}
       
   595 				break;
       
   596 			case 'is_cat':
       
   597 				$term = get_term_by('id', $value, 'category');
       
   598 				$value = (!$term) ? 0 : $term->term_id;
       
   599 				break;
       
   600 			default:
       
   601 				$value = apply_filters('aktt_sanitize_setting', $value, $key, $type);
       
   602 		}
       
   603 		return $value;
       
   604 	}
       
   605 	
       
   606 	
       
   607 	/**
       
   608 	 * Outputs the plugin's settings form.  Utilizes the "settings" API in WP
       
   609 	 *
       
   610 	 * @return void
       
   611 	 */
       
   612 	static function settings_page() {
       
   613 		global $wpdb;
       
   614 		$wpdb->aktt = $wpdb->prefix.'ak_twitter';
       
   615 		$upgrade_needed = in_array($wpdb->aktt, $wpdb->get_col("
       
   616 			SHOW TABLES
       
   617 		"));
       
   618 		if ($upgrade_needed) {
       
   619 			$upgrade_col = false;
       
   620 			$cols = $wpdb->get_results("
       
   621 				DESCRIBE $wpdb->aktt
       
   622 			");
       
   623 			foreach ($cols as $col) {
       
   624 				if ($col->Field == 'upgrade_30') {
       
   625 					$upgrade_col = true;
       
   626 					break;
       
   627 				}
       
   628 			}
       
   629 			if ($upgrade_col) {
       
   630 				$upgrade_needed = (bool) $wpdb->get_var("
       
   631 					SELECT COUNT(*)
       
   632 					FROM $wpdb->aktt
       
   633 					WHERE upgrade_30 = 0
       
   634 				");
       
   635 			}
       
   636 		}
       
   637 // check to see if CRON for backfilling data is scheduled
       
   638 		if (wp_next_scheduled('aktt_backfill_tweets') === false) {
       
   639 // check to see if it should be
       
   640 			$query = new WP_Query(array(
       
   641 				'post_type' => AKTT::$post_type,
       
   642 				'posts_per_page' => 10,
       
   643 				'meta_key' => '_aktt_30_backfill_needed',
       
   644 			));
       
   645 			if (count($query->posts)) {
       
   646 // schedule
       
   647 				wp_schedule_event(time() + 900, 'hourly', 'aktt_backfill_tweets');
       
   648 			}
       
   649 			unset($query);
       
   650 		}
       
   651 		self::get_social_accounts();
       
   652 		include(AKTT_PATH.'/views/admin.php');
       
   653 	}
       
   654 	
       
   655 	
       
   656 	/**
       
   657 	 * Returns the nonce'd URL for manually kicking off updates
       
   658 	 *
       
   659 	 * @return string
       
   660 	 */
       
   661 	static function get_manual_update_url() {
       
   662 		$url = add_query_arg(array('aktt_action' => 'manual_tweet_download'), admin_url('index.php'));
       
   663 		return wp_nonce_url($url, 'manual_tweet_download');
       
   664 	}
       
   665 	
       
   666 	
       
   667 	/**
       
   668 	 * Loads the social twitter accounts into a static variable
       
   669 	 *
       
   670 	 * @return void
       
   671 	 */
       
   672 	static function get_social_accounts() {
       
   673 		$social_twitter = Social::instance()->service('twitter');
       
   674 		
       
   675 		// If we don't have a Social_Twitter object, get out
       
   676 		if (is_null($social_twitter)) {
       
   677 			return;
       
   678 		}
       
   679 		
       
   680 		// If we don't have any Social Twitter accounts, get out
       
   681 		$social_accounts = $social_twitter->accounts();
       
   682 		if (empty($social_accounts)) {
       
   683 			return;
       
   684 		}
       
   685 		
       
   686 		/* Loop over our social twitter accounts and create AKTT_Account objects 
       
   687 		that will store the various configuration options for the twitter accounts. */
       
   688 		foreach ($social_accounts as $obj_id => $acct_obj) {
       
   689 			// If this account has already been assigned, continue on
       
   690 			if (isset(self::$accounts[$obj_id]) || !$acct_obj->universal()) {
       
   691 				continue;
       
   692 			}
       
   693 			
       
   694 			/* Call a static method to load the object, so we 
       
   695 			can ensure it was instantiated properly */
       
   696 			$o = AKTT_Account::load($acct_obj);
       
   697 			
       
   698 			// Assign the object, only if we were successfully created
       
   699 			if (is_a($o, 'AKTT_Account')) {
       
   700 				self::$accounts[$obj_id] = $o;
       
   701 			}
       
   702 		}
       
   703 	}
       
   704 	
       
   705 	/**
       
   706 	 * Return the first account from the list, at random.
       
   707 	 *
       
   708 	 * @return mixed AKTT_Account object|bool
       
   709 	 */
       
   710 	static function default_account() {
       
   711 		self::get_social_accounts();
       
   712 		if (count(self::$accounts)) {
       
   713 			foreach (self::$accounts as $account) {
       
   714 				if ($account->option('enabled')) {
       
   715 					return $account;
       
   716 				}
       
   717 			}
       
   718 		}
       
   719 		return false;
       
   720 	}
       
   721 	
       
   722 	/**
       
   723 	 * Remove an account when it is removed from Social
       
   724 	 *
       
   725 	 * @return void
       
   726 	 */
       
   727 	static function social_account_disconnected($service, $id) {
       
   728 		if ($service == 'twitter') {
       
   729 			$accounts = get_option('aktt_v3_accounts');
       
   730 			if (is_array($accounts) && count($accounts) && isset($accounts[$id])) {
       
   731 				$account = Social::instance()->service('twitter')->account($id);
       
   732 				// If the account being removed was only a universal account, it will no longer
       
   733 				// be available (false). If it is still around as a personal account (but is not
       
   734 				// a universal account), then the !universal() check will handle that.
       
   735 				if ($account === false or !$account->universal()) {
       
   736 					unset($accounts[$id]);
       
   737 					update_option('aktt_v3_accounts', $accounts);
       
   738 				}
       
   739 			}
       
   740 		}
       
   741 	}
       
   742 	
       
   743 	/**
       
   744 	 * Iterates over all the twitter accounts in social and downloads and imports the tweets.
       
   745 	 *
       
   746 	 * @return void
       
   747 	 */
       
   748 	function import_tweets() {
       
   749 		// load our accounts
       
   750 		self::get_social_accounts();
       
   751 		
       
   752 		// See if we have any accounts to loop over
       
   753 		if (!is_array(self::$accounts) || empty(self::$accounts)) {
       
   754 			return;
       
   755 		}
       
   756 		
       
   757 		// iterate over each account and download the tweets
       
   758 		foreach (self::$accounts as $id => $acct) {
       
   759 			// Download the tweets for that acct
       
   760 			if ($acct->option('enabled')) {
       
   761 				// could time out with lots of accounts, so a new request for each
       
   762 				$url = home_url('index.php').'?'.http_build_query(array(
       
   763 					'aktt_action' => 'download_account_tweets',
       
   764 					'acct_id' => $id,
       
   765 					'social_api_key' => Social::option('system_cron_api_key')
       
   766 				), null, '&');
       
   767 				self::log('Downloading tweets for '.$acct->social_acct->name().': '.$url);
       
   768 				wp_remote_get(
       
   769 					$url,
       
   770 					array(
       
   771 						'timeout' => 0.01,
       
   772 						'blocking' => false,
       
   773 						'sslverify' => apply_filters('https_local_ssl_verify', true),
       
   774 					)
       
   775 				);
       
   776 			}
       
   777 		}
       
   778 	}
       
   779 	
       
   780 	
       
   781 	/**
       
   782 	 * Find 10 tweets, backfill the data from Twitter
       
   783 	 *
       
   784 	 * @param int $count 
       
   785 	 * @return bool
       
   786 	 */
       
   787 	function backfill_tweets($count = 10) {
       
   788 		self::log('#### Backfilling tweets ####');
       
   789 		$query = new WP_Query(array(
       
   790 			'post_type' => AKTT::$post_type,
       
   791 			'posts_per_page' => 10,
       
   792 			'meta_key' => '_aktt_30_backfill_needed',
       
   793 		));
       
   794 		if (!count($query->posts)) {
       
   795 			if (($timestamp = wp_next_scheduled('aktt_backfill_tweets')) !== false) {
       
   796 				wp_unschedule_event($timestamp, 'aktt_backfill_tweets');
       
   797 			}
       
   798 			return false;
       
   799 		}
       
   800 		foreach ($query->posts as $post) {
       
   801 			$tweet_id = get_post_meta($post->ID, '_aktt_tweet_id', true);
       
   802 			if (empty($tweet_id)) {
       
   803 				continue;
       
   804 			}
       
   805 			$url = home_url('index.php').'?'.http_build_query(array(
       
   806 				'aktt_action' => 'backfill_tweet_data',
       
   807 				'tweet_id' => $tweet_id,
       
   808 				'social_api_key' => Social::option('system_cron_api_key')
       
   809 			), null, '&');
       
   810 			self::log('Backfilling tweet '.$tweet_id.' '.$url);
       
   811 			wp_remote_get(
       
   812 				$url,
       
   813 				array(
       
   814 					'timeout' => 0.01,
       
   815 					'blocking' => false,
       
   816 					'sslverify' => apply_filters('https_local_ssl_verify', true),
       
   817 				)
       
   818 			);
       
   819 		}
       
   820 		return true;
       
   821 	}
       
   822 	
       
   823 	/**
       
   824 	 * Create tweet when Social does a broadcast
       
   825 	 *
       
   826 	 * @param Social_Response $response 
       
   827 	 * @param string $key
       
   828 	 * @param stdClass $post
       
   829 	 * @return void
       
   830 	 */
       
   831 	static function social_broadcast_response($response, $key, $post) {
       
   832 // get tweet
       
   833 		$data = $response->body();
       
   834 		$tweet = $data->response;
       
   835 // check if it's one of our enabled accounts
       
   836 		self::get_social_accounts();
       
   837 		foreach (self::$accounts as $account) {
       
   838 			if ($account->option('enabled') && $account->social_acct->id() == $tweet->user->id) {
       
   839 // populate AKTT_Tweet object, save
       
   840 				$t = new AKTT_Tweet($tweet);
       
   841 				$t->add();
       
   842 				break;
       
   843 			}
       
   844 		}
       
   845 	}
       
   846 	
       
   847 	/**
       
   848 	 * Check for auth against Social's api key
       
   849 	 *
       
   850 	 * @return book
       
   851 	 */
       
   852 	static function social_key_auth() {
       
   853 		return (bool) (!empty($_GET['social_api_key']) && stripslashes($_GET['social_api_key']) == Social::option('system_cron_api_key'));
       
   854 	}
       
   855 	
       
   856 	
       
   857 	/**
       
   858 	 * Request handler
       
   859 	 *
       
   860 	 * @return void
       
   861 	 */
       
   862 	function controller(){
       
   863 		if (isset($_GET['aktt_action'])) {
       
   864 			switch ($_GET['aktt_action']) {
       
   865 				case 'download_account_tweets':
       
   866 					if (empty($_GET['acct_id']) || !AKTT::social_key_auth()) {
       
   867 						wp_die(__('Sorry, try again.', 'twitter-tools'));
       
   868 					}
       
   869 					$acct_id = intval($_GET['acct_id']);
       
   870 					self::get_social_accounts();
       
   871 					if (isset(self::$accounts[$acct_id])) {
       
   872 						if ($tweets = self::$accounts[$acct_id]->download_tweets()) {
       
   873 							self::$accounts[$acct_id]->save_tweets($tweets);
       
   874 						}
       
   875 					}
       
   876 					die();
       
   877 					break;
       
   878 				case 'import_tweet':
       
   879 // check for status_id && auth key
       
   880 					if (empty($_GET['tweet_id']) || !AKTT::social_key_auth()) {
       
   881 						wp_die(__('Sorry, try again.', 'twitter-tools'));
       
   882 					}
       
   883 // check for account_name
       
   884 					$username = (!empty($_GET['username']) ? stripslashes($_GET['username']) : null);
       
   885 // download tweet
       
   886 					$tweet = self::download_tweet($_GET['tweet_id'], $username);
       
   887 					if (!is_a($tweet, 'stdClass')) {
       
   888 						wp_die('Failed to download tweet.');
       
   889 					}
       
   890 // store tweet
       
   891 					$t = new AKTT_Tweet($tweet);
       
   892 					if (!$t->exists_by_guid()) {
       
   893 						$t->add();
       
   894 					}
       
   895 					die();
       
   896 					break;
       
   897 				case 'backfill_tweet_data':
       
   898 					if (empty($_GET['tweet_id']) || !AKTT::social_key_auth()) {
       
   899 						wp_die(__('Sorry, try again.', 'twitter-tools'));
       
   900 					}
       
   901 					$t = new AKTT_Tweet(stripslashes($_GET['tweet_id']));
       
   902 					if (!$t->get_post()) {
       
   903 						die();
       
   904 					}
       
   905 					$usernames = wp_get_object_terms($t->post->ID, 'aktt_accounts');
       
   906 					$username = $usernames[0]->slug;
       
   907 					
       
   908 					$tweet = self::download_tweet($_GET['tweet_id'], $username);
       
   909 					
       
   910 					if (!is_a($tweet, 'stdClass')) {
       
   911 						wp_die('Failed to download tweet');
       
   912 					}
       
   913 					$t->update_twitter_data($tweet);
       
   914 					die();
       
   915 					break;
       
   916 			}
       
   917 		}
       
   918 	}
       
   919 	
       
   920 	
       
   921 	/**
       
   922 	 * Request handler for admin
       
   923 	 *
       
   924 	 * @return void
       
   925 	 */
       
   926 	function admin_controller(){
       
   927 		if (isset($_GET['aktt_action'])) {
       
   928 			switch ($_GET['aktt_action']) {
       
   929 				case 'manual_tweet_download':
       
   930 					// Permission & nonce checking
       
   931 					if (!check_admin_referer('manual_tweet_download') || !current_user_can(self::$cap_download)) { 
       
   932 						wp_die(__('Sorry, try again.', 'twitter-tools'));
       
   933 					}
       
   934 					
       
   935 					self::import_tweets();
       
   936 					echo json_encode(array(
       
   937 						'result' => 'success',
       
   938 						'msg' => __('Tweets are downloading&hellip;', 'twitter-tools')
       
   939 					));
       
   940 					die();
       
   941 					break;
       
   942 				case 'upgrade-3.0':
       
   943 					// Permission checking
       
   944 					if (!current_user_can(self::$cap_options)) { 
       
   945 						wp_die(__('Sorry, try again.', 'twitter-tools'));
       
   946 					}
       
   947 					include(AKTT_PATH.'/upgrade/3.0.php');
       
   948 					aktt_upgrade_30();
       
   949 					die();
       
   950 					break;
       
   951 				case 'upgrade-3.0-run':
       
   952 					// Permission checking
       
   953 					if (!current_user_can(self::$cap_options) || !wp_verify_nonce($_GET['nonce'], 'upgrade-3.0-run')) { 
       
   954 						header('Content-type: application/json');
       
   955 						echo json_encode(array(
       
   956 							'result' => 'error',
       
   957 							'message' => __('Sorry, try again.', 'twitter-tools')
       
   958 						));
       
   959 						die();
       
   960 					}
       
   961 					include(AKTT_PATH.'/upgrade/3.0.php');
       
   962 					$to_upgrade = aktt_upgrade_30_run();
       
   963 					header('Content-type: application/json');
       
   964 					echo json_encode(array(
       
   965 						'result' => 'success',
       
   966 						'to_upgrade' => $to_upgrade
       
   967 					));
       
   968 					die();
       
   969 					break;
       
   970 				case 'tweets_updated':
       
   971 					self::add_admin_notice(__('Tweets are downloading...', 'twitter-tools'));
       
   972 					break;
       
   973 			}
       
   974 		}
       
   975 	}
       
   976 	
       
   977 	
       
   978 	/**
       
   979 	 * Load JS resources necessary for admin... only on the twitter tools' settings page
       
   980 	 *
       
   981 	 * @param string $hook_suffix 
       
   982 	 * @return void
       
   983 	 */
       
   984 	function admin_enqueue_scripts($hook_suffix) {
       
   985 		add_action('admin_footer', array('AKTT', 'admin_js'));
       
   986 		if ($hook_suffix == 'settings_page_twitter-tools') {
       
   987 			wp_enqueue_script('suggest');
       
   988 			add_action('admin_footer', array('AKTT', 'admin_js_suggest'));
       
   989 		}
       
   990 	}
       
   991 	
       
   992 	
       
   993 	/**
       
   994 	 * Output the admin-side JavaScript
       
   995 	 *
       
   996 	 * @return void
       
   997 	 */
       
   998 	static function admin_js() {
       
   999 ?>
       
  1000 <script type="text/javascript">
       
  1001 jQuery(function($) {
       
  1002 	$('a[href="post-new.php?post_type=aktt_tweet"]').hide().parent('li').hide();
       
  1003 	if (location.href.indexOf('edit-tags.php?taxonomy=aktt_accounts') != -1 ||
       
  1004 		location.href.indexOf('edit-tags.php?taxonomy=aktt_mentions') != -1 ||
       
  1005 		location.href.indexOf('edit-tags.php?taxonomy=aktt_hashtags') != -1 ||
       
  1006 		location.href.indexOf('edit-tags.php?taxonomy=aktt_types') != -1
       
  1007 	) {
       
  1008 		$('#col-left .form-wrap').hide();
       
  1009 	}
       
  1010 });
       
  1011 </script>
       
  1012 <?php
       
  1013 	}
       
  1014 
       
  1015 	/**
       
  1016 	 * Output the admin-side JavaScript for auto-suggest
       
  1017 	 *
       
  1018 	 * @return void
       
  1019 	 */
       
  1020 	static function admin_js_suggest() {
       
  1021 ?>
       
  1022 <script type="text/javascript">
       
  1023 jQuery(function($) {
       
  1024 	$('.type-ahead').each(function() {
       
  1025 		var tax = $(this).data('tax');
       
  1026 		$(this).suggest(
       
  1027 			ajaxurl + '?action=ajax-tag-search&tax=' + tax,
       
  1028 			{ 
       
  1029 				delay: 500, 
       
  1030 				minchars: 2,
       
  1031 				multiple: true 
       
  1032 			}
       
  1033 		);
       
  1034 	});
       
  1035 });
       
  1036 </script>
       
  1037 <?php
       
  1038 	}
       
  1039 	
       
  1040 	function log($msg) {
       
  1041 		if (self::$debug) {
       
  1042 			error_log($msg);
       
  1043 		}
       
  1044 	}
       
  1045 	
       
  1046 	static function profile_url($username) {
       
  1047 		return 'http://twitter.com/'.urlencode($username);
       
  1048 	}
       
  1049 
       
  1050 	static function profile_link($username) {
       
  1051 		return '<a href="'.esc_url(self::profile_url($username)).'">'.esc_html(self::profile_prefix($username)).'</a>';
       
  1052 	}
       
  1053 
       
  1054 	static function profile_prefix($username, $prefix = '@') {
       
  1055 		if (AKTT::substr($username, 0, 1) != '#') {
       
  1056 			$username = '@'.$username;
       
  1057 		}
       
  1058 		return $username;
       
  1059 	}
       
  1060 
       
  1061 	static function hashtag_url($hashtag) {
       
  1062 		$hashtag = self::hashtag_prefix($hashtag);
       
  1063 		return 'http://twitter.com/search?q='.urlencode($hashtag);
       
  1064 	}
       
  1065 
       
  1066 	static function hashtag_link($hashtag) {
       
  1067 		$hashtag = self::hashtag_prefix($hashtag);
       
  1068 		return '<a href="'.esc_url(self::hashtag_url($hashtag)).'">'.esc_html($hashtag).'</a>';
       
  1069 	}
       
  1070 	
       
  1071 	static function hashtag_prefix($hashtag, $prefix = '#') {
       
  1072 		if (AKTT::substr($hashtag, 0, 1) != '#') {
       
  1073 			$hashtag = '#'.$hashtag;
       
  1074 		}
       
  1075 		return $hashtag;
       
  1076 	}
       
  1077 	
       
  1078 	static function status_url($username, $id) {
       
  1079 		return 'http://twitter.com/'.urlencode($username).'/status/'.urlencode($id);
       
  1080 	}
       
  1081 	
       
  1082 	static function download_tweet($status_id, $username = null) {
       
  1083 		if (empty(AKTT::$accounts)) {
       
  1084 			return false;
       
  1085 		}
       
  1086 		$account_found = $tweet = false;
       
  1087 		if (!empty($username)) {
       
  1088 			AKTT::get_social_accounts();
       
  1089 			foreach (AKTT::$accounts as $id => $account) {
       
  1090 				if ($username == $account->social_acct->name()) {
       
  1091 					// proper account stored as $account
       
  1092 					$account_found = true;
       
  1093 					break;
       
  1094 				}
       
  1095 			}
       
  1096 			if (!$account_found) {
       
  1097 				$account = AKTT::$accounts[0]; // use any account
       
  1098 			}
       
  1099 			$response = Social::instance()->service('twitter')->request(
       
  1100 				$account->social_acct,
       
  1101 				'statuses/show/'.urlencode($t->id).'.json',
       
  1102 					array(
       
  1103 					'include_entities' => 1, // include explicit hashtags and mentions
       
  1104 					'include_rts' => 1, // include retweets
       
  1105 				)
       
  1106 			);
       
  1107 			$content = $response->body();
       
  1108 			if ($content->result == 'success') {
       
  1109 				$tweets = $content->response;
       
  1110 				if (!$tweets || !is_array($tweets) || count($tweets) != 1) {
       
  1111 					$tweet = $tweet[0];
       
  1112 				}
       
  1113 			}
       
  1114 		}
       
  1115 		return $tweet;
       
  1116 	}
       
  1117 	
       
  1118 	static function gmt_to_wp_time($gmt_time) {
       
  1119 		$timezone_string = get_option('timezone_string');
       
  1120 		if (!empty($timezone_string)) {
       
  1121 			// Not using get_option('gmt_offset') because it gets the offset for the
       
  1122 			// current date/time which doesn't work for timezones with daylight savings time.
       
  1123 			$gmt_date = date('Y-m-d H:i:s', $gmt_time);
       
  1124 			$datetime = new DateTime($gmt_date);
       
  1125 			$datetime->setTimezone(new DateTimeZone(get_option('timezone_string')));
       
  1126 			$offset_in_secs = $datetime->getOffset();
       
  1127 			
       
  1128 			return $gmt_time + $offset_in_secs;
       
  1129 		}
       
  1130 		else {
       
  1131 			return $gmt_time + (get_option('gmt_offset') * 3600);
       
  1132 		}
       
  1133 	}
       
  1134 
       
  1135 	static function substr_replace($string, $replacement, $start, $length = null, $encoding = null) {
       
  1136 		// from http://www.php.net/manual/en/function.substr-replace.php#90146
       
  1137 		// via https://github.com/ruanyf/wp-twitter-tools/commit/56d1a4497483b2b39f434fdfab4797d8574088e5
       
  1138 		if (extension_loaded('mbstring') === true) {
       
  1139 			$string_length = (is_null($encoding) === true) ? mb_strlen($string) : mb_strlen($string, $encoding);
       
  1140 			
       
  1141 			if ($start < 0) {
       
  1142 				$start = max(0, $string_length + $start);
       
  1143 			}
       
  1144 			else if ($start > $string_length) {
       
  1145 				$start = $string_length;
       
  1146 			}
       
  1147 			if ($length < 0) {
       
  1148 				$length = max(0, $string_length - $start + $length);
       
  1149 			}
       
  1150 			else if ((is_null($length) === true) || ($length > $string_length)) {
       
  1151 				$length = $string_length;
       
  1152 			}
       
  1153 			if (($start + $length) > $string_length) {
       
  1154 				$length = $string_length - $start;
       
  1155 			}
       
  1156 			if (is_null($encoding) === true) {
       
  1157 				return mb_substr($string, 0, $start) . $replacement 
       
  1158 					. mb_substr($string, $start + $length, $string_length - $start - $length);
       
  1159 			}
       
  1160 			return mb_substr($string, 0, $start, $encoding) . $replacement 
       
  1161 				. mb_substr($string, $start + $length, $string_length - $start - $length, $encoding);
       
  1162 		}
       
  1163 		else {
       
  1164 			return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length);
       
  1165 		}
       
  1166 	}
       
  1167 
       
  1168 	static function strlen($str, $encoding = null) {
       
  1169 		if (function_exists('mb_strlen')) {
       
  1170 			if (is_null($encoding) === true) {
       
  1171 				return mb_strlen($str);
       
  1172 			}
       
  1173 			else {
       
  1174 				return mb_strlen($str, $encoding);
       
  1175 			}
       
  1176 		}
       
  1177 		else {
       
  1178 			return strlen($str);
       
  1179 		}
       
  1180 	}
       
  1181 
       
  1182 	static function substr($str, $start, $length) {
       
  1183 		if (function_exists('mb_substr')) {
       
  1184 			return mb_substr($str, $start, $length);
       
  1185 		}
       
  1186 		else {
       
  1187 			return substr($str, $start, $length);
       
  1188 		}
       
  1189 	}
       
  1190 
       
  1191 }
       
  1192 
       
  1193