Russian TYPO3 community Форум больше не используется. Присоединяйтесь к каналу #community-ru в Slack for TYPO3 community  

Вернуться   Russian TYPO3 community > Обсуждение общих технических вопросов > Общие вопросы

Ответ
 
Опции темы Опции просмотра
Старый 30.08.2011, 20:49   #1
elezeo
Продвинутый
 
Аватар для elezeo
 
Регистрация: 05.07.2011
Адрес: Россия, Интернет
Сообщений: 69
По умолчанию t3blog длина статьи

Приветствую всех вебмастеров.
Вопросик такой, совсем простой, но разобраться сам не смог.
Установил блог, настроил, всё работает. Но только вот в списке статей (в так называемом режиме blogList) статьи высвечиваются полностью, без обрезки.
А как сделать, чтобы в режиме списка статьи выводились не полностью а к примеру 1 или 2 абзаца и потом ссылка Подробнее (обрезалось). А уже в режиме Single выводилась полностью.
Может я чего недонастроил, но в мануале ничего подоббного не нашел
Подскажите, плиз.
elezeo вне форума   Ответить с цитированием
Старый 01.09.2011, 01:59   #2
elezeo
Продвинутый
 
Аватар для elezeo
 
Регистрация: 05.07.2011
Адрес: Россия, Интернет
Сообщений: 69
По умолчанию

В общем в результате долгих мучений всё-таки сделал как надо.
Для того чтобы обрезать контент разработчики придумали маркер ###MORE### который надо вставить в тексте контента и в список блогов выведеться только то, что до маркера.

Но не тут то было!!!
Текст действительно становиться меньше, но обрезается он не в месте маркера, а ниже и отображается еще примерно такое же количество текста.

Долго ломал голову и ковырял код t3blog расширения. Вроде бы все верно делается.
Смущало лишь одно.. В функции, где делается обрезка получается не верная позиция маркера ###MORE###
Тут до меня и дошло, что при работе с кодировкой UTF-8 функция strpos видит больше байтов в строке, кодированной utf-8 и потому позиция находиться не та.

Итак я нашел функцию utf8_strpos и внедрил ее в код.
Вот какие изменения я внес:
файл: .../ext/t3blog/pi1/lib/class.t3blog_div.php
Код:
class t3blog_div {

    static public function utf8_strpos($haystack, $needle, $offset = 0){ 
      // get substring (if isset offset param) 
      $offset = ($offset<0) ? 0 : $offset; 
      if ($offset>0){ 
         preg_match('/^.{' . $offset . '}(.*)/us', $haystack, $dummy); 
         $haystack = (isset($dummy[1])) ? $dummy[1] : ''; 
      } 

      // get relative pos 
      $p = strpos($haystack, $needle); 
      if ($haystack=='' or $p===false) return false; 
      $r = $offset; 
      $i = 0; 

      // calc real pos 
      while($i<$p){ 
         if (ord($haystack[$i])<128) { 
            # ascii symbol 
            $i = $i + 1; 
         } else { 
            # non-ascii symbol with variable length 
            # (handling first byte) 
            $bvalue = decbin(ord($haystack[$i])); 
            $i = $i + strlen(preg_replace('/^(1+)(.+)$/', '\1', $bvalue)); 
         } 
         $r++; 
      } 
      return $r; 
    }
//........тут дальнейший код
Код:
	static public function fetchContentData($postId, array &$contentUidArray, &$hasDivider, &$textBeforeDivider) {
.....какой-то код
  if ($rowContent['CType'] == 'text' || $rowContent['CType'] == 'textpic') {
      $dividerPosition = t3blog_div::utf8_strpos($rowContent['bodytext'], '###MORE###');
.....какой-то код
}
и еще там же в другой функции.. найдите эту строку
Код:
			$textBeforeDivider = $cObj->cropHTML($renderedText,
				($dividerPosition + 10). '|');
и замените "10" на 14
Код:
			$textBeforeDivider = $cObj->cropHTML($renderedText,
				($dividerPosition + 14). '|');
Это они немного накасячили, забыв включить в размер закрытый тег </p>
Всё должно работать.

Думаю пригодиться!

Последний раз редактировалось elezeo; 01.09.2011 в 02:31
elezeo вне форума   Ответить с цитированием
Старый 01.09.2011, 13:37   #3
Андрей Аксенов
Senior Member
 
Регистрация: 12.04.2008
Адрес: Россия, Москва
Сообщений: 706
Отправить сообщение для Андрей Аксенов с помощью ICQ Отправить сообщение для Андрей Аксенов с помощью Skype™
По умолчанию

да, там в t3blog косяков много... например до сих пор не решили вопрос с количеством включаемых в статью элементов содержимого - зачем-то ограничили количество в 100 элементов... пришлось тоже доделывать... до сих пор не работает нормально RSS, руки не доходят разобраться что там и как... Сама настройка блога тоже желает лучшего, по этому поводу написал не одну статью... За доработку спасибо, хорошо бы это еще на сайте разработки расширения написать: http://forge.typo3.org/projects/show/extension-t3blog
Андрей Аксенов вне форума   Ответить с цитированием
Старый 02.09.2011, 21:46   #4
elezeo
Продвинутый
 
Аватар для elezeo
 
Регистрация: 05.07.2011
Адрес: Россия, Интернет
Сообщений: 69
По умолчанию

RSS я смог запустить - Действительно через Ж. Но, как говориться - тяжело в учении - легко в бою.
Если надо - могу поделиться.
А обрезчик всё равно неправильно у меня режет, но как-нибудь докопаюсь и сделаю правильно.
elezeo вне форума   Ответить с цитированием
Старый 02.09.2011, 23:36   #5
Андрей Аксенов
Senior Member
 
Регистрация: 12.04.2008
Адрес: Россия, Москва
Сообщений: 706
Отправить сообщение для Андрей Аксенов с помощью ICQ Отправить сообщение для Андрей Аксенов с помощью Skype™
По умолчанию

Ну да, насчет обрезчика, чуток по-другому, но не так, как хотелось бы... А насчет RSS поделись, если не сложно. По поводу настроек уже ни раз писал на форум разработки, но в ответ - тишина...
Андрей Аксенов вне форума   Ответить с цитированием
Старый 03.09.2011, 02:54   #6
elezeo
Продвинутый
 
Аватар для elezeo
 
Регистрация: 05.07.2011
Адрес: Россия, Интернет
Сообщений: 69
По умолчанию

Цитата:
Сообщение от Андрей Аксенов Посмотреть сообщение
Ну да, насчет обрезчика, чуток по-другому, но не так, как хотелось бы... А насчет RSS поделись, если не сложно. По поводу настроек уже ни раз писал на форум разработки, но в ответ - тишина...
Хоть и не в тему топика, но все-равно держи...
Скопируй этот код в отдельный файл TS и проинклюдь его к себе в шаблон.

Код:
# BLOG layout
plugin.tx_t3blog_pi1.layoutBlog = COA
plugin.tx_t3blog_pi1.layoutBlog {
	wrap = <div class="t3blog">|<div class="clear"><!-- --></div></div>

	#Pagetitle
	1  = TEXT
	1 {		
		value = {$TEXT.pagetitle}
		wrap = <h1 id="title">|</h1>
		typolink.parameter = {$PID.startpage}		
	}

	#Header
	10 = COA
	10 {
		wrap = <div id="header">|</div>
		## Now load the navigation
		10 < temp.navigation

		# TagClouds
		20 = COA
		20 {
			stdWrap = |
			required = 1
			10 = TEXT
			10 {
				field = tagCloud
			}
		}
		30 = TEXT
		30 {
			wrap = <div id="header_bottom">|</div>
			value = <!--snowflake.ch-->
		}
	}

	#left Content
	20 = COA
	20 {
		stdWrap {
			wrap = <div id="col1">|</div>
			required = 1
		}
			10 = TEXT
		10 {
			value = <!-- -->
		}
	}
	#left/main Content
	#CONTENT
	30 = COA
	30 {
		stdWrap {
			wrap = <div id="col2" class="content">|</div>
			required = 1
		}
		10 = TEXT
		10 {
			field = blogList
		}
		30 = COA
		30 {
			stdWrap {
				prepend = TEXT
				prepend {
					//value = navigation
					wrap = <hr class="hidden" /><h2 id="mainContentLabel" class="hidden">|</h2>
				}
				wrap = <div id ="mainContent" class="content">|</div>				
			}
			20 < styles.content.get
			20.select.andWhere = irre_parenttable = '' AND irre_parentid = 0
		}
	}
	#right Content
	40 = COA
	40 {
		stdWrap {
			wrap = <div id="col3">|</div>
			required = 1
		}
		5 = TEXT
		5 {
			field = searchBox
		}
		10 = TEXT
		10 {
			field = feed
		}
		20 = TEXT
		20 {
			field = calendar
		}
		30 = TEXT
		30 {
			field = latestPostNav
		}
		40 = TEXT
		40 {
			field = categories
		}
		50 = TEXT
		50 {
			field = blogrollList
		}
		60 = TEXT
		60 {
			field = latestCommentsNav
		}
		70 = TEXT
		70 {
			field = archive
		}
		80 = TEXT
		80 {
			field = rss
		}
		90 = TEXT
		90 {
			field = socialBookmarks
		}
		100 = TEXT
		100 {
			field = views
		}
	}
}

xmlnews = PAGE
xmlnews {
	typeNum = 100
	config {
		disableAllHeaderCode = 1
		additionalHeaders = Content-type:text/xml
		linkVars = L
		no_cache = 1
 		xhtml_cleaning = 0
 		admPanel = 0
 	}
 	10 = USER
	10 {
		# Call the user function
		userFunc = tx_t3blog_pi1->main
		widget.rss < plugin.tx_t3blog_pi1.rss

		# Load "design" of the RSS-stuff
		# Attention! This will only work, when you are
		# using the static template "T3BLOG Layout".
		# The static template has included the full design of
		# a blog (position of widgets and much more)

		template =< plugin.tx_t3blog_pi1.layoutBlog.40.80
	}
}
Я в общем то взял его из кодов самого расширения и ничено не менял.
Единственное только настройки фидов менял как было описано в одном из блогов по настройке t3blog
elezeo вне форума   Ответить с цитированием
Старый 03.09.2011, 23:36   #7
Андрей Аксенов
Senior Member
 
Регистрация: 12.04.2008
Адрес: Россия, Москва
Сообщений: 706
Отправить сообщение для Андрей Аксенов с помощью ICQ Отправить сообщение для Андрей Аксенов с помощью Skype™
По умолчанию

ну это немного не то... как настраивать t3blog я как раз у себя в журнале рассказывал...
Андрей Аксенов вне форума   Ответить с цитированием
Старый 03.09.2011, 23:48   #8
elezeo
Продвинутый
 
Аватар для elezeo
 
Регистрация: 05.07.2011
Адрес: Россия, Интернет
Сообщений: 69
По умолчанию

Цитата:
Сообщение от Андрей Аксенов Посмотреть сообщение
ну это немного не то... как настраивать t3blog я как раз у себя в журнале рассказывал...
А что тогда?
elezeo вне форума   Ответить с цитированием
Старый 04.09.2011, 00:24   #9
Андрей Аксенов
Senior Member
 
Регистрация: 12.04.2008
Адрес: Россия, Москва
Сообщений: 706
Отправить сообщение для Андрей Аксенов с помощью ICQ Отправить сообщение для Андрей Аксенов с помощью Skype™
По умолчанию

это я про RSS, у меня оно на самом деле хоть и настроено, но в реальности не транслируется. Вот и думал, где собака порылась...
Андрей Аксенов вне форума   Ответить с цитированием
Старый 21.01.2012, 15:42   #10
bit
Новенький
 
Аватар для bit
 
Регистрация: 08.02.2011
Сообщений: 9
По умолчанию

Всем привет.
Вот ковыряю t3blog, так и не получается обрезать пост....
Вот оригинал моего кода:
Код:
<?php

class t3blog_div {


	/**
	 * Crops the text at divider's position with respect to the HTML structure.
	 *
	 * @param array $row
	 * @param int $dividerPosition
	 * @return string
	 */
	static public function cropText(array $row, $dividerPosition) {
		$cObj = t3lib_div::makeInstance('tslib_cObj');
		if (method_exists($cObj, 'cropHTML')) {
			// Algorithm:
			// - render text correctly
			// - make sure there is no empty paragraphs for ###MORE###
			$renderedText = t3blog_div::getSingle($row, 'tt_content', $GLOBALS['TSFE']->tmpl->setup);
			$textBeforeDivider = $cObj->cropHTML($renderedText,
				($dividerPosition + 10). '|');
			$regExp = '/<p(?:\s[^>]*)?>\s*###MORE###\s*<\/p>/';
			if (preg_match($regExp, $textBeforeDivider)) {
				$textBeforeDivider = preg_replace($regExp, '', $textBeforeDivider);
			}
			else {
				$textBeforeDivider = str_replace('###MORE###', '', $textBeforeDivider);
			}
		}
		else {
			$textBeforeDivider = substr($row['bodytext'], 0, $dividerPosition);
		}
		return $textBeforeDivider;
	}

	/**
	 * Fetches content data for the current post.
	 *
	 * @param int $postId
	 * @param array $contentUidArray
	 * @param boolean $hasDivider
	 * @param string $textBeforeDivider
	 */
	static public function fetchContentData($postId, array &$contentUidArray, &$hasDivider, &$textBeforeDivider) {
		static $cache = array();

		if (isset($cache[$postId])) {
			$contentUidArray = $cache[$postId]['contentUidArray'];
			$hasDivider = $cache[$postId]['hasDivider'];
			$textBeforeDivider = $cache[$postId]['textBeforeDivider'];
		}
		else {
			$cObj = t3lib_div::makeInstance('tslib_cObj');
			$res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
				'uid,bodytext,CType', 'tt_content',
				'irre_parentid=' . $postId .
					' AND irre_parenttable=\'tx_t3blog_post\'' .
					$cObj->enableFields('tt_content'),
				'', 'sorting'
			);
			$contentUidArray = array();
			$hasDivider = false;
			$textBeforeDivider = '';
			while (false !== ($rowContent = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res))) {
				if ($rowContent['CType'] == 'text' || $rowContent['CType'] == 'textpic') {
					$dividerPosition = strpos($rowContent['bodytext'], '###MORE###');
					if ($dividerPosition !== false) {
						$textBeforeDivider = self::cropText($rowContent, $dividerPosition);
						$hasDivider = true;
						break;
					}
				}
				$contentUidArray[] = $rowContent['uid'];
			}
			$GLOBALS['TYPO3_DB']->sql_free_result($res);

			$cache[$postId] = array(
				'contentUidArray' => $contentUidArray,
				'hasDivider' => $hasDivider,
				'textBeforeDivider' => $textBeforeDivider
			);
		}
	}


	/**
	 * Parses data through typoscript.
	 *
	 * @param array $data Data which will be passed to the typoscript.
	 * @param string $tsObjectKey The typoscript which will be called.
	 * @param array $tsObjectConf TS object configuration
	 * @return string
	 */
	static public function getSingle(array $data, $tsObjectKey, array $tsObjectConf) {
		$cObj = t3lib_div::makeInstance('tslib_cObj');
		$cObj->data = $data;
		return $cObj->cObjGetSingle($tsObjectConf[$tsObjectKey], $tsObjectConf[$tsObjectKey . '.']);
	}

	/**
	 * Checks if it is a valid email. Use t3lib_div::validEmail() instead!
	 *
	 * @param 	string 	$email: emailaddress
	 * @return 	boolean	true if error
	 * @deprecated
	 */
	static public function checkEmail($email){
		return !t3lib_div::validEmail($email);
	}

	/**
	 * Checks if it is a valid http:// url
	 * adds "http://" string if there is none.
	 *
	 * @param 	string 	$url: url-address
	 * @return 	boolean	true if error
	 * @deprecated Use t3lib_div::isValidUrl()
	 */
	static public function checkExternalUrl($url) {
		return t3lib_div::isValidUrl($url);
	}

	/**
	 * Returns the username (realname) from be_user by a uid
	 *
	 * @param  int $uid uid of the be_user
	 * @return string Real name of the be_user
	 */
	static public function getAuthorByUid($uid) {
		list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('realName', 'be_users',
			'uid= ' . intval($uid), '', '', '1'
		);
		return (is_array($row) ? $row['realName'] : '');
	}

	/**
	 * Obtains the category name by its uid
	 *
	 * @param int $uid uid of a specific category
	 * @return string name of the category
	 */
	static public function getCategoryNameByUid($uid) {
		list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
			'catname', 'tx_t3blog_cat', 'uid=' . intval($uid),
			'', '', '1'
		);
		return (is_array($row) ? $row['catname'] : '');
	}


	/**
	 * returns the page browser of given table.
	 *
	 * @author snowflake <typo3@snowflake.ch>
	 *
	 * @param int 			$numOfEntries: total of all elements of a table
	 * @param string 		$ident: identifier for pointer. e.g. recently editet as more than 1 page browser on the site.
	 * @param $prefixId 	$todo: functionname of what-is-to-do-after-page-click-function
	 *
	 * @return string 		HTML-Content to the browser
	 */
	static public function getPageBrowser($numOfEntries, $ident, $prefixId, $llarray, $piVars, $conf, $limit = 10, $maxPages = 20)	{
  		// Get default configuration
  		$pageBrowserConf = $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_pagebrowse_pi1.'];
		if (!is_array($pageBrowserConf) || !isset($pageBrowserConf['userFunc'])) {
			// Hardcoded because:
			// - language labels are unavailable from here
			// - this message is for installers only, not for end users
			$result = 'Warning: "pagebrowse" extension TypoScript is unavailable! Did you forget to include it before t3blog\'s TypoScript?';
		}
		else {
			$pages = intval($numOfEntries/$limit) + (($numOfEntries % $limit) == 0 ? 0 : 1);
			if ($pages == 0) {
				$result = '';
			}
			else {
				// Modify this configuration
				$pageBrowserConf = array_merge($pageBrowserConf, array(
					'pageParameterName' => 'tx_t3blog_post_pointer',
					'numberOfPages' => $pages,
					'numberOfLinks' => isset($conf['numberOfPageBrowserLinks']) ? intval($conf['numberOfPageBrowserLinks']) : 0
				));

				self::setPageBrowserFilters($pageBrowserConf);

				// Get page browser
				$cObj = t3lib_div::makeInstance('tslib_cObj');
				/* @var $cObj tslib_cObj */
				$cObj->start(array(), '');
				$result = $cObj->cObjGetSingle('USER', $pageBrowserConf);
			}
		}
		return $result;
	}

	/**
	 * Adds extra conditions to the page browser link
	 *
	 * @param array $conf
	 * @return void
	 */
	static protected function setPageBrowserFilters(array &$conf) {
		$postVars = t3lib_div::_GP('tx_t3blog_pi1');
		if (is_array($postVars) && isset($postVars['sword'])) {
			$conf['extraQueryString'] = t3lib_div::implodeArrayForUrl('tx_t3blog_pi1', array(
				'sword' => $postVars['sword']
			));
		}
	}

	/**
	 * Sets an alternative blog Pid.
	 *
	 * @param 	integer		$pid: pid of the record storage page
	 */
	static public function setAlternativeBlogPid($pid) {
		$GLOBALS['alternativeBlogPid'] = $pid;
	}

	/**
	 * Returns the blog storage folder pid
	 *
	 * @return integer 	pid of the storage folder
	 */
	static public function getBlogPid(){
		static $cachedPid = 0;

		if (isset($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_t3blog_pi1.']['blogPid']) &&
				t3lib_div::testInt($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_t3blog_pi1.']['blogPid'])) {
			return $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_t3blog_pi1.']['blogPid'];
		}

		// get pid
		if (isset($GLOBALS['alternativeBlogPid']) && $GLOBALS['alternativeBlogPid'] > 0) {
			return intval($GLOBALS['alternativeBlogPid']);
		}

		if ($cachedPid != 0) {
			$pid = $cachedPid;
		}
		else {
			// get the Rootline
			$rootline = array_reverse($GLOBALS['TSFE']->tmpl->rootLine);

			// go through rootline until a blogPid is found
			$pidList = array();
			foreach ($rootline as $page) {
				$pidList[] = $page['uid'];
			}
			$pidString = implode(',', $pidList);
			list($row) = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid', 'pages',
				'uid IN (' . $pidString . ') AND module=\'t3blog\'' .
				$GLOBALS['TSFE']->sys_page->enableFields('pages'),
				'', 'FIELD(uid,' . $pidString . ')', 1);
			if (is_array($row)) {
				$pid = $row['uid'];
			}
			else {
				$pid = $GLOBALS['TSFE']->id;
			}
			$cachedPid = $pid;
		}
		return intval($pid);
	}

	static public function getExpandCollapseJavaScript($elementId, $idPrefix, $openMarker, $closeMarker) {
		if (!isset($GLOBALS['TSFE']->additionalHeaderData['t3blog-collapse-js'])) {
			$GLOBALS['TSFE']->additionalHeaderData['t3blog-collapse-js'] = '<script ' .
				'src="http://ajax.googleapis.com/ajax/libs/ext-core/3/ext-core.js" ' .
				'type="text/javascript"></script><script type="text/javascript">/*<![CDATA[*/' .
					t3lib_div::minifyJavaScript(
				'function tx_t3blog_expand(element) {
					element.slideIn();
				}
				function tx_t3blog_collapse(element) {
					element.slideOut("t", {
						useDisplay: true
					});
				}
				function tx_t3blog_toggle(element, toggleElement, openMarker, closeMarker) {
					element = Ext.get(element);
					if (toggleElement) {
						toggleElement = Ext.get(toggleElement);
					}
					if (element.dom.style.display == "none") {
						tx_t3blog_expand(element);
						if (toggleElement) {
							toggleElement.dom.innerHTML = closeMarker;
						}
					}
					else {
						tx_t3blog_collapse(element);
						if (toggleElement) {
							toggleElement.dom.innerHTML = openMarker;
						}
					}
				}
				function tx_t3blog_expandCollapse(elementId, idPrefix, openMarker, closeMarker) {
					Ext.get("toggle" + elementId).on("click", function() {
						tx_t3blog_toggle(idPrefix + elementId, "toggle" + elementId, openMarker, closeMarker);
					});
				}'
						)
				. '/*]]>*/</script>';
		}

		$js = 'tx_t3blog_expandCollapse(' .
				'"' . addslashes($elementId) . '",' .
				'"' . addslashes($idPrefix) . '",' .
				'"' . addslashes($openMarker) . '",' .
				'"' . addslashes($closeMarker) . '"' .
				');';

		return $js;
	}
}

if (defined('TYPO3_MODE') && $TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/t3blog/pi1/lib/class.t3blog_div.php'])	{
	include_once($TYPO3_CONF_VARS[TYPO3_MODE]['XCLASS']['ext/t3blog/pi1/lib/class.t3blog_div.php']);
}

?>
Буду благодарен за любую помощь!!!
bit вне форума   Ответить с цитированием
Ответ

Теги
bloglist, crop, обрезка, strpos, t3blog

Опции темы
Опции просмотра

Ваши права в разделе
Вы не можете создавать новые темы
Вы не можете отвечать в темах
Вы не можете прикреплять вложения
Вы не можете редактировать свои сообщения

BB code is Вкл.
Смайлы Вкл.
[IMG] код Вкл.
HTML код Выкл.

Быстрый переход

Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Не могу настройть T3BLOG demav Общие вопросы 4 18.06.2011 15:20
t3blog 500 Internal Server Error Aprel Общие вопросы 0 23.06.2010 23:44
Security issues in third party TYPO3 extensions including t3blog RSS Bot Новости TYPO3 (на английском) 0 04.03.2010 18:50
tt_news: листание статьи в режиме SINGLE simon.kms TypoScript практикум 4 04.01.2008 23:33
Статьи из журнала T3N Magazine Comrade Новости TYPO3-разработки 1 12.07.2006 16:12


Часовой пояс GMT +4, время: 01:40.


Работает на vBulletin® версия 3.8.1.
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Перевод: zCarot

Хостинг и техническая поддержка: TYPO3 Лаборатория