97d7d62117dba4f3620ca2a5f210332eabfad86f
[Packages/TYPO3.CMS.git] / components / testing_framework / Classes / Core / Testbase.php
1 <?php
2 namespace TYPO3\Components\TestingFramework\Core;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use Doctrine\DBAL\DBALException;
18 use Doctrine\DBAL\DriverManager;
19 use TYPO3\CMS\Core\Core\Bootstrap;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
22 use TYPO3\CMS\Core\Database\Schema\SqlReader;
23 use TYPO3\CMS\Core\Utility\ArrayUtility;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26 /**
27 * This is a helper class used by unit, functional and acceptance test
28 * environment builders.
29 * It contains methods to create test environments.
30 *
31 * This class is for internal use only and may change wihtout further notice.
32 *
33 * Use the classes "UnitTestCase", "FunctionalTestCase" or "AcceptanceCoreEnvironment"
34 * to indirectly benefit from this class in own extensions.
35 */
36 class Testbase
37 {
38 /**
39 * This class must be called in CLI environment as a security measure
40 * against path disclosures and other stuff. Check this within
41 * constructor to make sure this check can't be circumvented.
42 */
43 public function __construct()
44 {
45 // Ensure cli only as security measure
46 if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') {
47 die('This script supports command line usage only. Please check your command.');
48 }
49 }
50
51 /**
52 * Makes sure error messages during the tests get displayed no matter what is set in php.ini.
53 *
54 * @return void
55 */
56 public function enableDisplayErrors()
57 {
58 @ini_set('display_errors', 1);
59 }
60
61 /**
62 * Defines a list of basic constants that are used by GeneralUtility and other
63 * helpers during tests setup. Those are sanitized in SystemEnvironmentBuilder
64 * to be not defined again.
65 *
66 * @return void
67 * @see SystemEnvironmentBuilder::defineBaseConstants()
68 */
69 public function defineBaseConstants()
70 {
71 // A null, a tabulator, a linefeed, a carriage return, a substitution, a CR-LF combination
72 defined('NUL') ?: define('NUL', chr(0));
73 defined('TAB') ?: define('TAB', chr(9));
74 defined('LF') ?: define('LF', chr(10));
75 defined('CR') ?: define('CR', chr(13));
76 defined('SUB') ?: define('SUB', chr(26));
77 defined('CRLF') ?: define('CRLF', CR . LF);
78
79 if (!defined('TYPO3_OS')) {
80 // Operating system identifier
81 // Either "WIN" or empty string
82 $typoOs = '';
83 if (!stristr(PHP_OS, 'darwin') && !stristr(PHP_OS, 'cygwin') && stristr(PHP_OS, 'win')) {
84 $typoOs = 'WIN';
85 }
86 define('TYPO3_OS', $typoOs);
87 }
88 }
89
90 /**
91 * Defines the PATH_site and PATH_thisScript constant and sets $_SERVER['SCRIPT_NAME'].
92 * For unit tests only
93 *
94 * @return void
95 */
96 public function defineSitePath()
97 {
98 define('PATH_site', $this->getWebRoot());
99 define('PATH_thisScript', PATH_site . 'typo3/cli_dispatch.phpsh');
100 $_SERVER['SCRIPT_NAME'] = PATH_thisScript;
101
102 if (!file_exists(PATH_thisScript)) {
103 $this->exitWithMessage('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
104 }
105 }
106
107 /**
108 * Defines the constant ORIGINAL_ROOT for the path to the original TYPO3 document root.
109 * For functional / acceptance tests only
110 * If ORIGINAL_ROOT already is defined, this method is a no-op.
111 *
112 * @return void
113 */
114 public function defineOriginalRootPath()
115 {
116 if (!defined('ORIGINAL_ROOT')) {
117 define('ORIGINAL_ROOT', $this->getWebRoot());
118 }
119
120 if (!file_exists(ORIGINAL_ROOT . 'typo3/cli_dispatch.phpsh')) {
121 $this->exitWithMessage('Unable to determine path to entry script. Please check your path or set an environment variable \'TYPO3_PATH_WEB\' to your root path.');
122 }
123 }
124
125 /**
126 * Define TYPO3_MODE to BE
127 *
128 * @return void
129 */
130 public function defineTypo3ModeBe()
131 {
132 define('TYPO3_MODE', 'BE');
133 }
134
135 /**
136 * Sets the environment variable TYPO3_CONTEXT to testing.
137 *
138 * @return void
139 */
140 public function setTypo3TestingContext()
141 {
142 putenv('TYPO3_CONTEXT=Testing');
143 }
144
145 /**
146 * Creates directories, recursively if required.
147 *
148 * @param string $directory Absolute path to directories to create
149 * @return void
150 * @throws Exception
151 */
152 public function createDirectory($directory)
153 {
154 if (is_dir($directory)) {
155 return;
156 }
157 @mkdir($directory, 0777, true);
158 clearstatcache();
159 if (!is_dir($directory)) {
160 throw new Exception('Directory "' . $directory . '" could not be created', 1404038665);
161 }
162 }
163
164 /**
165 * Checks whether given test instance exists in path and is younger than some minutes.
166 * Used in functional tests
167 *
168 * @param string $instancePath Absolute path to test instance
169 * @return bool
170 */
171 public function recentTestInstanceExists($instancePath)
172 {
173 if (@file_get_contents($instancePath . '/last_run.txt') <= (time() - 300)) {
174 return false;
175 } else {
176 // Test instance exists and is pretty young -> re-use
177 return true;
178 }
179 }
180
181 /**
182 * Remove test instance folder structure if it exists.
183 * This may happen if a functional test before threw a fatal or is too old
184 *
185 * @param string $instancePath Absolute path to test instance
186 * @return void
187 * @throws Exception
188 */
189 public function removeOldInstanceIfExists($instancePath)
190 {
191 if (is_dir($instancePath)) {
192 $success = GeneralUtility::rmdir($instancePath, true);
193 if (!$success) {
194 throw new Exception(
195 'Can not remove folder: ' . $instancePath,
196 1376657210
197 );
198 }
199 }
200 }
201
202 /**
203 * Create last_run.txt file within instance path containing timestamp of "now".
204 * Used in functional tests to reuse an instance for multiple tests in one test case.
205 *
206 * @param string $instancePath Absolute path to test instance
207 * @return void
208 */
209 public function createLastRunTextfile($instancePath)
210 {
211 // Store the time instance was created
212 file_put_contents($instancePath . '/last_run.txt', time());
213 }
214
215 /**
216 * Link TYPO3 CMS core from "parent" instance.
217 * For functional and acceptance tests.
218 *
219 * @param string $instancePath Absolute path to test instance
220 * @throws Exception
221 * @return void
222 */
223 public function setUpInstanceCoreLinks($instancePath)
224 {
225 $linksToSet = [
226 ORIGINAL_ROOT . 'typo3' => $instancePath . '/typo3',
227 ORIGINAL_ROOT . 'index.php' => $instancePath . '/index.php'
228 ];
229 foreach ($linksToSet as $from => $to) {
230 $success = symlink($from, $to);
231 if (!$success) {
232 throw new Exception(
233 'Creating link failed: from ' . $from . ' to: ' . $to,
234 1376657199
235 );
236 }
237 }
238 }
239
240 /**
241 * Link test extensions to the typo3conf/ext folder of the instance.
242 * For functional and acceptance tests.
243 *
244 * @param string $instancePath Absolute path to test instance
245 * @param array $extensionPaths Contains paths to extensions relative to document root
246 * @throws Exception
247 * @return void
248 */
249 public function linkTestExtensionsToInstance($instancePath, array $extensionPaths)
250 {
251 foreach ($extensionPaths as $extensionPath) {
252 $absoluteExtensionPath = ORIGINAL_ROOT . $extensionPath;
253 if (!is_dir($absoluteExtensionPath)) {
254 throw new Exception(
255 'Test extension path ' . $absoluteExtensionPath . ' not found',
256 1376745645
257 );
258 }
259 $destinationPath = $instancePath . '/typo3conf/ext/' . basename($absoluteExtensionPath);
260 $success = symlink($absoluteExtensionPath, $destinationPath);
261 if (!$success) {
262 throw new Exception(
263 'Can not link extension folder: ' . $absoluteExtensionPath . ' to ' . $destinationPath,
264 1376657142
265 );
266 }
267 }
268 }
269
270 /**
271 * Link paths inside the test instance, e.g. from a fixture fileadmin subfolder to the
272 * test instance fileadmin folder.
273 * For functional and acceptance tests.
274 *
275 * @param string $instancePath Absolute path to test instance
276 * @param array $pathsToLinkInTestInstance Contains paths as array of source => destination in key => value pairs of folders relative to test instance root
277 * @throws Exception if a source path could not be found and on failing creating the symlink
278 * @return void
279 */
280 public function linkPathsInTestInstance($instancePath, array $pathsToLinkInTestInstance)
281 {
282 foreach ($pathsToLinkInTestInstance as $sourcePathToLinkInTestInstance => $destinationPathToLinkInTestInstance) {
283 $sourcePath = $instancePath . '/' . ltrim($sourcePathToLinkInTestInstance, '/');
284 if (!file_exists($sourcePath)) {
285 throw new Exception(
286 'Path ' . $sourcePath . ' not found',
287 1476109221
288 );
289 }
290 $destinationPath = $instancePath . '/' . ltrim($destinationPathToLinkInTestInstance, '/');
291 $success = symlink($sourcePath, $destinationPath);
292 if (!$success) {
293 throw new Exception(
294 'Can not link the path ' . $sourcePath . ' to ' . $destinationPath,
295 1389969623
296 );
297 }
298 }
299 }
300
301 /**
302 * Database settings for functional and acceptance tests can be either set by
303 * environment variables (recommended), or from an existing LocalConfiguration as fallback.
304 * The method fetches these.
305 *
306 * An unique name will be added to the database name later.
307 *
308 * @throws Exception
309 * @return array [DB][host], [DB][username], ...
310 */
311 public function getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration()
312 {
313 $databaseName = trim(getenv('typo3DatabaseName'));
314 $databaseHost = trim(getenv('typo3DatabaseHost'));
315 $databaseUsername = trim(getenv('typo3DatabaseUsername'));
316 $databasePassword = getenv('typo3DatabasePassword');
317 $databasePasswordTrimmed = trim($databasePassword);
318 $databasePort = trim(getenv('typo3DatabasePort'));
319 $databaseSocket = trim(getenv('typo3DatabaseSocket'));
320 $databaseDriver = trim(getenv('typo3DatabaseDriver'));
321 if ($databaseName || $databaseHost || $databaseUsername || $databasePassword || $databasePort || $databaseSocket) {
322 // Try to get database credentials from environment variables first
323 $originalConfigurationArray = [
324 'DB' => [
325 'Connections' => [
326 'Default' => [
327 'driver' => 'mysqli'
328 ],
329 ],
330 ],
331 ];
332 if ($databaseName) {
333 $originalConfigurationArray['DB']['Connections']['Default']['dbname'] = $databaseName;
334 }
335 if ($databaseHost) {
336 $originalConfigurationArray['DB']['Connections']['Default']['host'] = $databaseHost;
337 }
338 if ($databaseUsername) {
339 $originalConfigurationArray['DB']['Connections']['Default']['user'] = $databaseUsername;
340 }
341 if ($databasePassword !== false) {
342 $originalConfigurationArray['DB']['Connections']['Default']['password'] = $databasePasswordTrimmed;
343 }
344 if ($databasePort) {
345 $originalConfigurationArray['DB']['Connections']['Default']['port'] = $databasePort;
346 }
347 if ($databaseSocket) {
348 $originalConfigurationArray['DB']['Connections']['Default']['unix_socket'] = $databaseSocket;
349 }
350 if ($databaseDriver) {
351 $originalConfigurationArray['DB']['Connections']['Default']['driver'] = $databaseDriver;
352 }
353 } elseif (file_exists(ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php')) {
354 // See if a LocalConfiguration file exists in "parent" instance to get db credentials from
355 $originalConfigurationArray = require ORIGINAL_ROOT . 'typo3conf/LocalConfiguration.php';
356 } else {
357 throw new Exception(
358 'Database credentials for tests are neither set through environment'
359 . ' variables, and can not be found in an existing LocalConfiguration file',
360 1397406356
361 );
362 }
363 return $originalConfigurationArray['DB'];
364 }
365
366 /**
367 * Maximum length of database names is 64 chars in mysql. Test this is not exceeded
368 * after a suffix has been added.
369 *
370 * @param string $originalDatabaseName Base name of the database
371 * @param array $configuration "LocalConfiguration" array with DB settings
372 * @throws Exception
373 */
374 public function testDatabaseNameIsNotTooLong($originalDatabaseName, array $configuration)
375 {
376 // Maximum database name length for mysql is 64 characters
377 if (strlen($configuration['DB']['Connections']['Default']['dbname']) > 64) {
378 $suffixLength = strlen($configuration['DB']['Connections']['Default']['dbname']) - strlen($originalDatabaseName);
379 $maximumOriginalDatabaseName = 64 - $suffixLength;
380 throw new Exception(
381 'The name of the database that is used for the functional test (' . $originalDatabaseName . ')' .
382 ' exceeds the maximum length of 64 character allowed by MySQL. You have to shorten your' .
383 ' original database name to ' . $maximumOriginalDatabaseName . ' characters',
384 1377600104
385 );
386 }
387 }
388
389 /**
390 * Create LocalConfiguration.php file of the test instance.
391 * For functional and acceptance tests.
392 *
393 * @param string $instancePath Absolute path to test instance
394 * @param array $configuration Base configuration array
395 * @param array $overruleConfiguration Overrule factory and base configuration
396 * @throws Exception
397 * @return void
398 */
399 public function setUpLocalConfiguration($instancePath, array $configuration, array $overruleConfiguration)
400 {
401 // Base of final LocalConfiguration is core factory configuration
402 $finalConfigurationArray = require ORIGINAL_ROOT . 'typo3/sysext/core/Configuration/FactoryConfiguration.php';
403 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $configuration);
404 ArrayUtility::mergeRecursiveWithOverrule($finalConfigurationArray, $overruleConfiguration);
405 $result = $this->writeFile(
406 $instancePath . '/typo3conf/LocalConfiguration.php',
407 '<?php' . chr(10) .
408 'return ' .
409 ArrayUtility::arrayExport(
410 $finalConfigurationArray
411 ) .
412 ';'
413 );
414 if (!$result) {
415 throw new Exception('Can not write local configuration', 1376657277);
416 }
417 }
418
419 /**
420 * Compile typo3conf/PackageStates.php containing default packages like core,
421 * a test specific list of additional core extensions, and a list of
422 * test extensions.
423 * For functional and acceptance tests.
424 *
425 * @param string $instancePath Absolute path to test instance
426 * @param array $defaultCoreExtensionsToLoad Default list of core extensions to load
427 * @param array $additionalCoreExtensionsToLoad Additional core extensions to load
428 * @param array $testExtensionPaths Paths to extensions relative to document root
429 * @throws Exception
430 */
431 public function setUpPackageStates(
432 $instancePath,
433 array $defaultCoreExtensionsToLoad,
434 array $additionalCoreExtensionsToLoad,
435 array $testExtensionPaths
436 ) {
437 $packageStates = [
438 'packages' => [],
439 'version' => 5,
440 ];
441
442 // Register default list of extensions and set active
443 foreach ($defaultCoreExtensionsToLoad as $extensionName) {
444 $packageStates['packages'][$extensionName] = [
445 'packagePath' => 'typo3/sysext/' . $extensionName . '/'
446 ];
447 }
448
449 // Register additional core extensions and set active
450 foreach ($additionalCoreExtensionsToLoad as $extensionName) {
451 $packageStates['packages'][$extensionName] = [
452 'packagePath' => 'typo3/sysext/' . $extensionName . '/'
453 ];
454 }
455
456 // Activate test extensions that have been symlinked before
457 foreach ($testExtensionPaths as $extensionPath) {
458 $extensionName = basename($extensionPath);
459 $packageStates['packages'][$extensionName] = [
460 'packagePath' => 'typo3conf/ext/' . $extensionName . '/'
461 ];
462 }
463
464 $result = $this->writeFile(
465 $instancePath . '/typo3conf/PackageStates.php',
466 '<?php' . chr(10) .
467 'return ' .
468 ArrayUtility::arrayExport(
469 $packageStates
470 ) .
471 ';'
472 );
473
474 if (!$result) {
475 throw new Exception('Can not write PackageStates', 1381612729);
476 }
477 }
478
479 /**
480 * Create a low level connection to dbms, without selecting the target database.
481 * Drop existing database if it exists and create a new one.
482 *
483 * @param string $databaseName Database name of this test instance
484 * @param string $originalDatabaseName Original database name before suffix was added
485 * @throws \TYPO3\Components\TestingFramework\Core\Exception
486 * @return void
487 */
488 public function setUpTestDatabase($databaseName, $originalDatabaseName)
489 {
490 Bootstrap::getInstance()->initializeTypo3DbGlobal();
491
492 // Drop database if exists. Directly using the Doctrine DriverManager to
493 // work around connection caching in ConnectionPool
494 $connectionParameters = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default'];
495 unset($connectionParameters['dbname']);
496 $schemaManager = DriverManager::getConnection($connectionParameters)->getSchemaManager();
497
498 if (in_array($databaseName, $schemaManager->listDatabases(), true)) {
499 $schemaManager->dropDatabase($databaseName);
500 }
501
502 try {
503 $schemaManager->createDatabase($databaseName);
504 } catch (DBALException $e) {
505 $user = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['user'];
506 $host = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']['host'];
507 throw new Exception(
508 'Unable to create database with name ' . $databaseName . '. This is probably a permission problem.'
509 . ' For this instance this could be fixed executing:'
510 . ' GRANT ALL ON `' . $originalDatabaseName . '_%`.* TO `' . $user . '`@`' . $host . '`;'
511 . ' Original message thrown by database layer: ' . $e->getMessage(),
512 1376579070
513 );
514 }
515 }
516
517 /**
518 * Bootstrap basic TYPO3. This bootstraps TYPO3 far enough to initialize database afterwards.
519 * For functional and acceptance tests.
520 *
521 * @param string $instancePath Absolute path to test instance
522 * @return void
523 */
524 public function setUpBasicTypo3Bootstrap($instancePath)
525 {
526 $_SERVER['PWD'] = $instancePath;
527 $_SERVER['argv'][0] = 'index.php';
528
529 $classLoader = require rtrim(realpath($instancePath . '/typo3'), '\\/') . '/../vendor/autoload.php';
530 Bootstrap::getInstance()
531 ->initializeClassLoader($classLoader)
532 ->setRequestType(TYPO3_REQUESTTYPE_BE | TYPO3_REQUESTTYPE_CLI)
533 ->baseSetup()
534 ->loadConfigurationAndInitialize(true)
535 ->loadTypo3LoadedExtAndExtLocalconf(true)
536 ->setFinalCachingFrameworkCacheConfiguration()
537 ->defineLoggingAndExceptionConstants()
538 ->unsetReservedGlobalVariables();
539 }
540
541 /**
542 * Truncate all tables.
543 * For functional and acceptance tests.
544 *
545 * @throws Exception
546 * @return void
547 */
548 public function initializeTestDatabaseAndTruncateTables()
549 {
550 Bootstrap::getInstance()->initializeTypo3DbGlobal();
551
552 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
553 ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
554 $schemaManager = $connection->getSchemaManager();
555
556 foreach ($schemaManager->listTables() as $table) {
557 $connection->truncate($table->getName());
558 }
559 }
560
561 /**
562 * Load ext_tables.php files.
563 * For functional and acceptance tests.
564 *
565 * @return void
566 */
567 public function loadExtensionTables()
568 {
569 Bootstrap::getInstance()->loadExtensionTables();
570 }
571
572 /**
573 * Create tables and import static rows.
574 * For functional and acceptance tests.
575 *
576 * @return void
577 */
578 public function createDatabaseStructure()
579 {
580 $schemaMigrationService = GeneralUtility::makeInstance(SchemaMigrator::class);
581 $sqlReader = GeneralUtility::makeInstance(SqlReader::class);
582 $sqlCode = $sqlReader->getTablesDefinitionString(true);
583
584 $createTableStatements = $sqlReader->getCreateTableStatementArray($sqlCode);
585
586 $schemaMigrationService->install($createTableStatements);
587
588 $insertStatements = $sqlReader->getInsertStatementArray($sqlCode);
589 $schemaMigrationService->importStaticData($insertStatements);
590 }
591
592 /**
593 * Imports a data set represented as XML into the test database,
594 *
595 * @param string $path Absolute path to the XML file containing the data set to load
596 * @return void
597 * @throws Exception
598 */
599 public function importXmlDatabaseFixture($path)
600 {
601 if (!is_file($path)) {
602 throw new Exception(
603 'Fixture file ' . $path . ' not found',
604 1376746261
605 );
606 }
607
608 $fileContent = file_get_contents($path);
609 // Disables the functionality to allow external entities to be loaded when parsing the XML, must be kept
610 $previousValueOfEntityLoader = libxml_disable_entity_loader(true);
611 $xml = simplexml_load_string($fileContent);
612 libxml_disable_entity_loader($previousValueOfEntityLoader);
613 $foreignKeys = [];
614
615 /** @var $table \SimpleXMLElement */
616 foreach ($xml->children() as $table) {
617 $insertArray = [];
618
619 /** @var $column \SimpleXMLElement */
620 foreach ($table->children() as $column) {
621 $columnName = $column->getName();
622 $columnValue = null;
623
624 if (isset($column['ref'])) {
625 list($tableName, $elementId) = explode('#', $column['ref']);
626 $columnValue = $foreignKeys[$tableName][$elementId];
627 } elseif (isset($column['is-NULL']) && ($column['is-NULL'] === 'yes')) {
628 $columnValue = null;
629 } else {
630 $columnValue = (string)$table->$columnName;
631 }
632
633 $insertArray[$columnName] = $columnValue;
634 }
635
636 $tableName = $table->getName();
637 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
638 ->getConnectionForTable($tableName);
639 $connection->insert(
640 $tableName,
641 $insertArray
642 );
643 if (isset($table['id'])) {
644 $elementId = (string)$table['id'];
645 $foreignKeys[$tableName][$elementId] = $connection->lastInsertId($tableName);
646 }
647 }
648 }
649
650 /**
651 * Returns the absolute path the TYPO3 document root.
652 * This is the "original" document root, not the "instance" root for functional / acceptance tests.
653 *
654 * @return string the TYPO3 document root using Unix path separators
655 */
656 protected function getWebRoot()
657 {
658 if (getenv('TYPO3_PATH_WEB')) {
659 $webRoot = getenv('TYPO3_PATH_WEB');
660 } else {
661 $webRoot = getcwd();
662 }
663 return rtrim(strtr($webRoot, '\\', '/'), '/') . '/';
664 }
665
666 /**
667 * Send http headers, echo out a text message and exit with error code
668 *
669 * @param string $message
670 */
671 protected function exitWithMessage($message)
672 {
673 echo $message . LF;
674 exit(1);
675 }
676
677 /**
678 * Writes $content to the file $file. This is a simplified version
679 * of GeneralUtility::writeFile that does not fix permissions.
680 *
681 * @param string $file Filepath to write to
682 * @param string $content Content to write
683 * @return bool TRUE if the file was successfully opened and written to.
684 */
685 protected function writeFile($file, $content)
686 {
687 if ($fd = fopen($file, 'wb')) {
688 $res = fwrite($fd, $content);
689 fclose($fd);
690 if ($res === false) {
691 return false;
692 }
693 return true;
694 }
695 return false;
696 }
697 }