71 private $sourceAdapterId;
74 private $foreignNamespaces =
null;
77 private $mLogItemCallback;
80 private $mUploadCallback;
83 private $mRevisionCallback;
86 private $mPageCallback;
89 private $mSiteInfoCallback;
92 private $mPageOutCallback;
95 private $mNoticeCallback;
101 private $mImportUploads;
104 private $mImageBasePath;
107 private $mNoUpdates =
false;
110 private $pageOffset = 0;
116 private $countableCache = [];
119 private $disableStatisticsUpdate =
false;
155 $this->performer = $performer;
156 $this->config = $config;
157 $this->hookRunner =
new HookRunner( $hookContainer );
158 $this->contentLanguage = $contentLanguage;
159 $this->namespaceInfo = $namespaceInfo;
160 $this->titleFactory = $titleFactory;
161 $this->wikiPageFactory = $wikiPageFactory;
162 $this->uploadRevisionImporter = $uploadRevisionImporter;
163 $this->contentHandlerFactory = $contentHandlerFactory;
164 $this->slotRoleRegistry = $slotRoleRegistry;
166 if ( !in_array(
'uploadsource', stream_get_wrappers() ) ) {
167 stream_wrapper_register(
'uploadsource', UploadSourceAdapter::class );
181 $this->contentLanguage,
182 $this->namespaceInfo,
192 return $this->reader;
199 $this->
debug(
"FAILURE: $err" );
200 wfDebug(
"WikiImporter XML error: $err" );
207 if ( $this->mDebug ) {
215 public function warn( $data ) {
226 if ( is_callable( $this->mNoticeCallback ) ) {
227 call_user_func( $this->mNoticeCallback, $msg,
$params );
240 $this->mDebug = $debug;
248 $this->mNoUpdates = $noupdates;
258 $this->pageOffset = $nthPage;
268 return wfSetVar( $this->mNoticeCallback, $callback );
277 $previous = $this->mPageCallback;
278 $this->mPageCallback = $callback;
292 $previous = $this->mPageOutCallback;
293 $this->mPageOutCallback = $callback;
303 $previous = $this->mRevisionCallback;
304 $this->mRevisionCallback = $callback;
314 $previous = $this->mUploadCallback;
315 $this->mUploadCallback = $callback;
325 $previous = $this->mLogItemCallback;
326 $this->mLogItemCallback = $callback;
336 $previous = $this->mSiteInfoCallback;
337 $this->mSiteInfoCallback = $callback;
347 $this->importTitleFactory = $factory;
356 if ( $namespace ===
null ) {
360 $this->contentLanguage,
361 $this->namespaceInfo,
368 $this->namespaceInfo->exists( intval( $namespace ) )
370 $namespace = intval( $namespace );
373 $this->namespaceInfo,
390 $status = Status::newGood();
391 $nsInfo = $this->namespaceInfo;
392 if ( $rootpage ===
null ) {
396 $this->contentLanguage,
401 } elseif ( $rootpage !==
'' ) {
402 $rootpage = rtrim( $rootpage,
'/' );
403 $title = Title::newFromText( $rootpage );
405 if ( !$title || $title->isExternal() ) {
406 $status->fatal(
'import-rootpage-invalid' );
407 } elseif ( !$nsInfo->hasSubpages( $title->getNamespace() ) ) {
408 $displayNSText = $title->getNamespace() ===
NS_MAIN
410 : $this->contentLanguage->getNsText( $title->getNamespace() );
411 $status->fatal(
'import-rootpage-nosubpage', $displayNSText );
431 $this->mImageBasePath = $dir;
438 $this->mImportUploads = $import;
447 $this->externalUserNames =
new ExternalUserNames( $usernamePrefix, $assignKnownUsers );
465 $title = $titleAndForeignTitle[0];
466 $page = $this->wikiPageFactory->newFromTitle( $title );
467 $this->countableCache[
'title_' . $title->getPrefixedText()] = $page->isCountable();
477 if ( !$revision->getContentHandler()->canBeUsedOn( $revision->getTitle() ) ) {
478 $this->
notice(
'import-error-bad-location',
479 $revision->getTitle()->getPrefixedText(),
481 $revision->getModel(),
482 $revision->getFormat()
489 return $revision->importOldRevision();
491 $this->
notice(
'import-error-unserialize',
492 $revision->getTitle()->getPrefixedText(),
494 $revision->getModel(),
495 $revision->getFormat()
508 return $revision->importLogItem();
517 $status = $this->uploadRevisionImporter->import( $revision );
518 return $status->isGood();
531 $sRevCount, $pageInfo
540 $page = $this->wikiPageFactory->newFromTitle( $pageIdentity );
542 $page->loadPageData( IDBAccessObject::READ_LATEST );
543 $rev = $page->getRevisionRecord();
544 if ( $rev ===
null ) {
546 wfDebug( __METHOD__ .
': Skipping article count adjustment for ' . $pageIdentity .
547 ' because WikiPage::getRevisionRecord() returned null' );
549 $update = $page->newPageUpdater( $this->performer )->prepareUpdate();
550 $countKey =
'title_' . CacheKeyHelper::getKeyForPage( $pageIdentity );
551 $countable = $update->isCountable();
552 if ( array_key_exists( $countKey, $this->countableCache ) &&
553 $countable != $this->countableCache[$countKey] ) {
554 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [
555 'articles' => ( (
int)$countable - (
int)$this->countableCache[$countKey] )
561 $title = Title::newFromPageIdentity( $pageIdentity );
562 return $this->hookRunner->onAfterImportPage( $title, $foreignTitle,
563 $revCount, $sRevCount, $pageInfo );
571 private function siteInfoCallback( $siteInfo ) {
572 if ( $this->mSiteInfoCallback ) {
573 return call_user_func_array(
574 $this->mSiteInfoCallback,
587 if ( $this->mPageCallback ) {
588 call_user_func( $this->mPageCallback, $title );
600 private function pageOutCallback(
PageIdentity $pageIdentity, $foreignTitle, $revCount,
601 $sucCount, $pageInfo ) {
602 if ( $this->mPageOutCallback ) {
603 call_user_func_array( $this->mPageOutCallback, func_get_args() );
612 private function revisionCallback( $revision ) {
613 if ( $this->mRevisionCallback ) {
614 return call_user_func_array(
615 $this->mRevisionCallback,
628 private function logItemCallback( $revision ) {
629 if ( $this->mLogItemCallback ) {
630 return call_user_func_array(
631 $this->mLogItemCallback,
646 return $this->reader->getAttribute( $attr ) ??
'';
657 if ( $this->reader->isEmptyElement ) {
661 while ( $this->reader->read() ) {
662 switch ( $this->reader->nodeType ) {
663 case XMLReader::TEXT:
664 case XMLReader::CDATA:
665 case XMLReader::SIGNIFICANT_WHITESPACE:
666 $buffer .= $this->reader->value;
668 case XMLReader::END_ELEMENT:
673 $this->reader->close();
683 $this->syntaxCheckXML();
689 $oldDisable = @libxml_disable_entity_loader(
true );
691 $this->reader->read();
693 if ( $this->reader->localName !=
'mediawiki' ) {
695 @libxml_disable_entity_loader( $oldDisable );
696 $error = libxml_get_last_error();
698 throw new NormalizedException(
"XML error at line {line}: {message}", [
699 'line' => $error->line,
700 'message' => $error->message,
703 throw new UnexpectedValueException(
704 "Expected '<mediawiki>' tag, got '<{$this->reader->localName}>' tag."
708 $this->
debug(
"<mediawiki> tag is correct." );
710 $this->
debug(
"Starting primary dump processing loop." );
712 $keepReading = $this->reader->read();
715 while ( $keepReading ) {
716 $tag = $this->reader->localName;
717 if ( $this->pageOffset ) {
718 if ( $tag ===
'page' ) {
721 if ( $pageCount < $this->pageOffset ) {
722 $keepReading = $this->reader->next();
726 $type = $this->reader->nodeType;
728 if ( !$this->hookRunner->onImportHandleToplevelXMLTag( $this ) ) {
730 } elseif ( $tag ==
'mediawiki' && $type == XMLReader::END_ELEMENT ) {
732 } elseif ( $tag ==
'siteinfo' ) {
733 $this->handleSiteInfo();
734 } elseif ( $tag ==
'page' ) {
736 } elseif ( $tag ==
'logitem' ) {
737 $this->handleLogItem();
738 } elseif ( $tag !=
'#text' ) {
739 $this->
warn(
"Unhandled top-level XML tag $tag" );
745 $keepReading = $this->reader->next();
747 $this->
debug(
"Skip" );
749 $keepReading = $this->reader->read();
754 @libxml_disable_entity_loader( $oldDisable );
755 $this->reader->close();
761 private function handleSiteInfo() {
762 $this->debug(
"Enter site info handler." );
766 $normalFields = [
'sitename',
'base',
'generator',
'case' ];
768 while ( $this->reader->read() ) {
769 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
770 $this->reader->localName ==
'siteinfo' ) {
774 $tag = $this->reader->localName;
776 if ( $tag ==
'namespace' ) {
779 } elseif ( in_array( $tag, $normalFields ) ) {
784 $siteInfo[
'_namespaces'] = $this->foreignNamespaces;
785 $this->siteInfoCallback( $siteInfo );
788 private function handleLogItem() {
789 $this->
debug(
"Enter log item handler." );
793 $normalFields = [
'id',
'comment',
'type',
'action',
'timestamp',
794 'logtitle',
'params' ];
796 while ( $this->reader->read() ) {
797 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
798 $this->reader->localName ==
'logitem' ) {
802 $tag = $this->reader->localName;
804 if ( !$this->hookRunner->onImportHandleLogItemXMLTag( $this, $logInfo ) ) {
806 } elseif ( in_array( $tag, $normalFields ) ) {
808 } elseif ( $tag ==
'contributor' ) {
809 $logInfo[
'contributor'] = $this->handleContributor();
810 } elseif ( $tag !=
'#text' ) {
811 $this->
warn(
"Unhandled log-item XML tag $tag" );
815 $this->processLogItem( $logInfo );
822 private function processLogItem( $logInfo ) {
825 if ( isset( $logInfo[
'id'] ) ) {
826 $revision->setID( $logInfo[
'id'] );
828 $revision->setType( $logInfo[
'type'] );
829 $revision->setAction( $logInfo[
'action'] );
830 if ( isset( $logInfo[
'timestamp'] ) ) {
831 $revision->setTimestamp( $logInfo[
'timestamp'] );
833 if ( isset( $logInfo[
'params'] ) ) {
834 $revision->setParams( $logInfo[
'params'] );
836 if ( isset( $logInfo[
'logtitle'] ) ) {
839 $revision->setTitle( Title::newFromText( $logInfo[
'logtitle'] ) );
842 $revision->setNoUpdates( $this->mNoUpdates );
844 if ( isset( $logInfo[
'comment'] ) ) {
845 $revision->setComment( $logInfo[
'comment'] );
848 if ( isset( $logInfo[
'contributor'][
'username'] ) ) {
849 $revision->setUsername(
850 $this->externalUserNames->applyPrefix( $logInfo[
'contributor'][
'username'] )
852 } elseif ( isset( $logInfo[
'contributor'][
'ip'] ) ) {
853 $revision->setUserIP( $logInfo[
'contributor'][
'ip'] );
855 $revision->setUsername( $this->externalUserNames->addPrefix(
'Unknown user' ) );
858 return $this->logItemCallback( $revision );
861 private function handlePage() {
863 $this->
debug(
"Enter page handler." );
864 $pageInfo = [
'revisionCount' => 0,
'successfulRevisionCount' => 0 ];
867 $normalFields = [
'title',
'ns',
'id',
'redirect',
'restrictions' ];
872 while ( $skip ? $this->reader->next() : $this->reader->read() ) {
873 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
874 $this->reader->localName ==
'page' ) {
880 $tag = $this->reader->localName;
885 } elseif ( !$this->hookRunner->onImportHandlePageXMLTag( $this, $pageInfo ) ) {
887 } elseif ( in_array( $tag, $normalFields ) ) {
895 if ( $tag ==
'redirect' ) {
900 } elseif ( $tag ==
'revision' || $tag ==
'upload' ) {
901 if ( !isset( $title ) ) {
902 $title = $this->processTitle( $pageInfo[
'title'],
903 $pageInfo[
'ns'] ??
null );
906 if ( is_array( $title ) ) {
908 [ $pageInfo[
'_title'], $foreignTitle ] = $title;
916 if ( $tag ==
'revision' ) {
917 $this->handleRevision( $pageInfo );
919 $this->handleUpload( $pageInfo );
922 } elseif ( $tag !=
'#text' ) {
923 $this->
warn(
"Unhandled page XML tag $tag" );
933 if ( array_key_exists(
'_title', $pageInfo ) ) {
935 $title = $pageInfo[
'_title'];
936 $this->pageOutCallback(
940 $pageInfo[
'revisionCount'],
941 $pageInfo[
'successfulRevisionCount'],
950 private function handleRevision( &$pageInfo ) {
951 $this->
debug(
"Enter revision handler" );
954 $normalFields = [
'id',
'parentid',
'timestamp',
'comment',
'minor',
'origin',
955 'model',
'format',
'text',
'sha1' ];
959 while ( $skip ? $this->reader->next() : $this->reader->read() ) {
960 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
961 $this->reader->localName ==
'revision' ) {
965 $tag = $this->reader->localName;
967 if ( !$this->hookRunner->onImportHandleRevisionXMLTag(
968 $this, $pageInfo, $revisionInfo )
971 } elseif ( in_array( $tag, $normalFields ) ) {
973 } elseif ( $tag ==
'content' ) {
975 $revisionInfo[$tag][] = $this->handleContent();
976 } elseif ( $tag ==
'contributor' ) {
977 $revisionInfo[
'contributor'] = $this->handleContributor();
978 } elseif ( $tag !=
'#text' ) {
979 $this->
warn(
"Unhandled revision XML tag $tag" );
984 $pageInfo[
'revisionCount']++;
985 if ( $this->processRevision( $pageInfo, $revisionInfo ) ) {
986 $pageInfo[
'successfulRevisionCount']++;
990 private function handleContent() {
991 $this->
debug(
"Enter content handler" );
994 $normalFields = [
'role',
'origin',
'model',
'format',
'text' ];
998 while ( $skip ? $this->reader->next() : $this->reader->read() ) {
999 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
1000 $this->reader->localName ==
'content' ) {
1004 $tag = $this->reader->localName;
1006 if ( !$this->hookRunner->onImportHandleContentXMLTag(
1007 $this, $contentInfo )
1010 } elseif ( in_array( $tag, $normalFields ) ) {
1012 } elseif ( $tag !=
'#text' ) {
1013 $this->
warn(
"Unhandled content XML tag $tag" );
1018 return $contentInfo;
1028 private function makeContent(
PageIdentity $page, $revisionId, $contentInfo ) {
1029 $maxArticleSize = $this->config->get( MainConfigNames::MaxArticleSize );
1031 if ( !isset( $contentInfo[
'text'] ) ) {
1032 throw new InvalidArgumentException(
'Missing text field in import.' );
1039 if ( ( !isset( $contentInfo[
'model'] ) ||
1040 in_array( $contentInfo[
'model'], [
1048 strlen( $contentInfo[
'text'] ) > $maxArticleSize * 1024
1050 throw new RuntimeException(
'The text of ' .
1052 "the revision with ID $revisionId" :
1054 ) .
" exceeds the maximum allowable size ({$maxArticleSize} KiB)" );
1057 $role = $contentInfo[
'role'] ?? SlotRecord::MAIN;
1058 $model = $contentInfo[
'model'] ?? $this->slotRoleRegistry
1059 ->getRoleHandler( $role )
1060 ->getDefaultModel( $page );
1061 $handler = $this->contentHandlerFactory->getContentHandler( $model );
1063 $text = $handler->importTransform( $contentInfo[
'text'] );
1065 return $handler->unserializeContent( $text );
1073 private function processRevision( $pageInfo, $revisionInfo ) {
1076 $revId = $revisionInfo[
'id'] ?? 0;
1078 $revision->setID( $revisionInfo[
'id'] );
1081 $title = $pageInfo[
'_title'];
1082 $revision->setTitle( $title );
1084 $content = $this->makeContent( $title, $revId, $revisionInfo );
1085 $revision->setContent( SlotRecord::MAIN, $content );
1087 foreach ( $revisionInfo[
'content'] ?? [] as $slotInfo ) {
1088 if ( !isset( $slotInfo[
'role'] ) ) {
1089 throw new RuntimeException(
"Missing role for imported slot." );
1092 $content = $this->makeContent( $title, $revId, $slotInfo );
1093 $revision->setContent( $slotInfo[
'role'], $content );
1095 $revision->setTimestamp( $revisionInfo[
'timestamp'] ??
wfTimestampNow() );
1097 if ( isset( $revisionInfo[
'comment'] ) ) {
1098 $revision->setComment( $revisionInfo[
'comment'] );
1101 if ( isset( $revisionInfo[
'minor'] ) ) {
1102 $revision->setMinor(
true );
1104 if ( isset( $revisionInfo[
'contributor'][
'username'] ) ) {
1105 $revision->setUsername(
1106 $this->externalUserNames->applyPrefix( $revisionInfo[
'contributor'][
'username'] )
1108 } elseif ( isset( $revisionInfo[
'contributor'][
'ip'] ) ) {
1109 $revision->setUserIP( $revisionInfo[
'contributor'][
'ip'] );
1111 $revision->setUsername( $this->externalUserNames->addPrefix(
'Unknown user' ) );
1113 if ( isset( $revisionInfo[
'sha1'] ) ) {
1114 $revision->setSha1Base36( $revisionInfo[
'sha1'] );
1116 $revision->setNoUpdates( $this->mNoUpdates );
1118 return $this->revisionCallback( $revision );
1125 private function handleUpload( &$pageInfo ) {
1126 $this->
debug(
"Enter upload handler" );
1129 $normalFields = [
'timestamp',
'comment',
'filename',
'text',
1130 'src',
'size',
'sha1base36',
'archivename',
'rel' ];
1134 while ( $skip ? $this->reader->next() : $this->reader->read() ) {
1135 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
1136 $this->reader->localName ==
'upload' ) {
1140 $tag = $this->reader->localName;
1142 if ( !$this->hookRunner->onImportHandleUploadXMLTag( $this, $pageInfo ) ) {
1144 } elseif ( in_array( $tag, $normalFields ) ) {
1146 } elseif ( $tag ==
'contributor' ) {
1147 $uploadInfo[
'contributor'] = $this->handleContributor();
1148 } elseif ( $tag ==
'contents' ) {
1150 $encoding = $this->reader->getAttribute(
'encoding' );
1151 if ( $encoding ===
'base64' ) {
1152 $uploadInfo[
'fileSrc'] = $this->dumpTemp( base64_decode( $contents ) );
1153 $uploadInfo[
'isTempSrc'] =
true;
1155 } elseif ( $tag !=
'#text' ) {
1156 $this->
warn(
"Unhandled upload XML tag $tag" );
1161 if ( $this->mImageBasePath && isset( $uploadInfo[
'rel'] ) ) {
1162 $path =
"{$this->mImageBasePath}/{$uploadInfo['rel']}";
1163 if ( file_exists(
$path ) ) {
1164 $uploadInfo[
'fileSrc'] =
$path;
1165 $uploadInfo[
'isTempSrc'] =
false;
1169 if ( $this->mImportUploads ) {
1170 return $this->processUpload( $pageInfo, $uploadInfo );
1178 private function dumpTemp( $contents ) {
1179 $filename = tempnam(
wfTempDir(),
'importupload' );
1180 file_put_contents( $filename, $contents );
1189 private function processUpload( $pageInfo, $uploadInfo ) {
1191 $revId = $pageInfo[
'id'];
1192 $title = $pageInfo[
'_title'];
1194 $uploadInfo[
'text'] ??=
'';
1195 $content = $this->makeContent( $title, $revId, $uploadInfo );
1197 $revision->setTitle( $title );
1198 $revision->setID( $revId );
1199 $revision->setTimestamp( $uploadInfo[
'timestamp'] );
1200 $revision->setContent( SlotRecord::MAIN, $content );
1201 $revision->setFilename( $uploadInfo[
'filename'] );
1202 if ( isset( $uploadInfo[
'archivename'] ) ) {
1203 $revision->setArchiveName( $uploadInfo[
'archivename'] );
1205 $revision->setSrc( $uploadInfo[
'src'] );
1206 if ( isset( $uploadInfo[
'fileSrc'] ) ) {
1207 $revision->setFileSrc( $uploadInfo[
'fileSrc'],
1208 !empty( $uploadInfo[
'isTempSrc'] )
1211 if ( isset( $uploadInfo[
'sha1base36'] ) ) {
1212 $revision->setSha1Base36( $uploadInfo[
'sha1base36'] );
1214 $revision->setSize( intval( $uploadInfo[
'size'] ) );
1215 $revision->setComment( $uploadInfo[
'comment'] );
1217 if ( isset( $uploadInfo[
'contributor'][
'username'] ) ) {
1218 $revision->setUsername(
1219 $this->externalUserNames->applyPrefix( $uploadInfo[
'contributor'][
'username'] )
1221 } elseif ( isset( $uploadInfo[
'contributor'][
'ip'] ) ) {
1222 $revision->setUserIP( $uploadInfo[
'contributor'][
'ip'] );
1224 $revision->setNoUpdates( $this->mNoUpdates );
1226 return call_user_func( $this->mUploadCallback, $revision );
1232 private function handleContributor() {
1233 $this->
debug(
"Enter contributor handler." );
1235 if ( $this->reader->isEmptyElement ) {
1239 $fields = [
'id',
'ip',
'username' ];
1242 while ( $this->reader->read() ) {
1243 if ( $this->reader->nodeType == XMLReader::END_ELEMENT &&
1244 $this->reader->localName ==
'contributor' ) {
1248 $tag = $this->reader->localName;
1250 if ( in_array( $tag, $fields ) ) {
1263 private function processTitle( $text, $ns =
null ) {
1264 if ( $this->foreignNamespaces ===
null ) {
1266 $this->contentLanguage
1270 $this->foreignNamespaces );
1273 $foreignTitle = $foreignTitleFactory->createForeignTitle( $text,
1276 $title = $this->importTitleFactory->createTitleFromForeignTitle(
1279 if ( $title ===
null ) {
1280 # Invalid page title? Ignore the page
1281 $this->
notice(
'import-error-invalid', $foreignTitle->getFullText() );
1283 } elseif ( $title->isExternal() ) {
1284 $this->
notice(
'import-error-interwiki', $title->getPrefixedText() );
1286 } elseif ( !$title->canExist() ) {
1287 $this->
notice(
'import-error-special', $title->getPrefixedText() );
1289 } elseif ( !$this->performer->definitelyCan(
'edit', $title ) ) {
1290 # Do not import if the importing wiki user cannot edit this page
1291 $this->
notice(
'import-error-edit', $title->getPrefixedText() );
1295 return [ $title, $foreignTitle ];
1302 private function openReader() {
1306 $oldDisable = @libxml_disable_entity_loader(
false );
1308 if ( PHP_VERSION_ID >= 80000 ) {
1310 $reader = XMLReader::open(
1311 'uploadsource://' . $this->sourceAdapterId,
null, LIBXML_PARSEHUGE );
1312 if ( $reader instanceof XMLReader ) {
1313 $this->reader = $reader;
1320 $this->reader =
new XMLReader;
1321 $status = $this->reader->open(
1322 'uploadsource://' . $this->sourceAdapterId,
null, LIBXML_PARSEHUGE );
1325 $error = libxml_get_last_error();
1327 @libxml_disable_entity_loader( $oldDisable );
1328 throw new RuntimeException(
1329 'Encountered an internal error while initializing WikiImporter object: ' . $error->message
1333 @libxml_disable_entity_loader( $oldDisable );
1339 private function syntaxCheckXML() {
1343 AtEase::suppressWarnings();
1344 $oldDisable = libxml_disable_entity_loader(
false );
1346 while ( $this->reader->read() );
1347 $error = libxml_get_last_error();
1349 $errorMessage =
'XML error at line ' . $error->line .
': ' . $error->message;
1350 wfDebug( __METHOD__ .
': Invalid xml found - ' . $errorMessage );
1351 throw new RuntimeException( $errorMessage );
1354 libxml_disable_entity_loader( $oldDisable );
1355 AtEase::restoreWarnings();
1356 $this->reader->close();
1361 $this->openReader();