base.inc 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. <?php
  2. /**
  3. * @file
  4. * Defines the base class for migration processes.
  5. */
  6. /**
  7. * The base class for all objects representing distinct steps in a migration
  8. * process. Most commonly these will be Migration objects which actually import
  9. * data from a source into a Drupal destination, but by deriving classes
  10. * directly from MigrationBase one can have other sorts of tasks (e.g.,
  11. * enabling/disabling of modules) occur during the migration process.
  12. */
  13. abstract class MigrationBase {
  14. /**
  15. * Track the migration currently running, so handlers can easily determine it
  16. * without having to pass a Migration object everywhere.
  17. *
  18. * @var Migration
  19. */
  20. protected static $currentMigration;
  21. public static function currentMigration() {
  22. return self::$currentMigration;
  23. }
  24. /**
  25. * The machine name of this Migration object, derived by removing the
  26. * 'Migration' suffix from the class name. Used to construct default
  27. * map/message table names, displayed in drush migrate-status, key to
  28. * migrate_status table...
  29. *
  30. * @var string
  31. */
  32. protected $machineName;
  33. public function getMachineName() {
  34. return $this->machineName;
  35. }
  36. /**
  37. * A migration group object, used to collect related migrations.
  38. *
  39. * @var MigrateGroup
  40. */
  41. protected $group;
  42. public function getGroup() {
  43. return $this->group;
  44. }
  45. /**
  46. * Detailed information describing the migration.
  47. *
  48. * @var string
  49. */
  50. protected $description;
  51. public function getDescription() {
  52. return $this->description;
  53. }
  54. public function setDescription($description) {
  55. $this->description = $description;
  56. }
  57. /**
  58. * Save options passed to current operation
  59. *
  60. * @var array
  61. */
  62. protected $options;
  63. public function getOption($option_name) {
  64. if (isset($this->options[$option_name])) {
  65. return $this->options[$option_name];
  66. }
  67. else {
  68. return NULL;
  69. }
  70. }
  71. public function getItemLimit() {
  72. if (isset($this->options['limit']) &&
  73. ($this->options['limit']['unit'] == 'items' || $this->options['limit']['unit'] == 'item')) {
  74. return $this->options['limit']['value'];
  75. }
  76. else {
  77. return NULL;
  78. }
  79. }
  80. public function getTimeLimit() {
  81. if (isset($this->options['limit']) &&
  82. ($this->options['limit']['unit'] == 'seconds' || $this->options['limit']['unit'] == 'second')) {
  83. return $this->options['limit']['value'];
  84. }
  85. else {
  86. return NULL;
  87. }
  88. }
  89. /**
  90. * Indicates that we are processing a rollback or import - used to avoid
  91. * excess writes in endProcess()
  92. *
  93. * @var boolean
  94. */
  95. protected $processing = FALSE;
  96. /**
  97. * Are we importing, rolling back, or doing nothing?
  98. *
  99. * @var enum
  100. */
  101. protected $status = MigrationBase::STATUS_IDLE;
  102. /**
  103. * When the current operation started.
  104. *
  105. * @var int
  106. */
  107. protected $starttime;
  108. /**
  109. * Whether to maintain a history of migration processes in migrate_log
  110. *
  111. * @var boolean
  112. */
  113. protected $logHistory = TRUE;
  114. /**
  115. * Primary key of the current history record (inserted at the beginning of
  116. * a process, to be updated at the end)
  117. *
  118. * @var int
  119. */
  120. protected $logID;
  121. /**
  122. * Number of "items" processed in the current migration process (whatever that
  123. * means for the type of process)
  124. *
  125. * @var int
  126. */
  127. protected $total_processed = 0;
  128. /**
  129. * List of other Migration classes which should be imported before this one.
  130. * E.g., a comment migration class would typically have node and user
  131. * migrations as dependencies.
  132. *
  133. * @var array
  134. */
  135. protected $dependencies = array(), $softDependencies = array();
  136. public function getHardDependencies() {
  137. return $this->dependencies;
  138. }
  139. public function setHardDependencies(array $dependencies) {
  140. $this->dependencies = $dependencies;
  141. }
  142. public function addHardDependencies(array $dependencies) {
  143. $this->dependencies = array_merge($this->dependencies, $dependencies);
  144. }
  145. public function getSoftDependencies() {
  146. return $this->softDependencies;
  147. }
  148. public function setSoftDependencies(array $dependencies) {
  149. $this->softDependencies = $dependencies;
  150. }
  151. public function addSoftDependencies(array $dependencies) {
  152. $this->softDependencies = array_merge($this->softDependencies, $dependencies);
  153. }
  154. public function getDependencies() {
  155. return array_merge($this->dependencies, $this->softDependencies);
  156. }
  157. /**
  158. * Name of a function for displaying feedback. It must take the message to
  159. * display as its first argument, and a (string) message type as its second
  160. * argument
  161. * (see drush_log()).
  162. *
  163. * @var string
  164. */
  165. protected static $displayFunction;
  166. public static function setDisplayFunction($display_function) {
  167. self::$displayFunction = $display_function;
  168. }
  169. /**
  170. * Track whether or not we've already displayed an encryption warning
  171. *
  172. * @var bool
  173. */
  174. protected static $showEncryptionWarning = TRUE;
  175. /**
  176. * The fraction of the memory limit at which an operation will be interrupted.
  177. * Can be overridden by a Migration subclass if one would like to push the
  178. * envelope. Defaults to 85%.
  179. *
  180. * @var float
  181. */
  182. protected $memoryThreshold = 0.85;
  183. /**
  184. * The PHP memory_limit expressed in bytes.
  185. *
  186. * @var int
  187. */
  188. protected $memoryLimit;
  189. /**
  190. * The fraction of the time limit at which an operation will be interrupted.
  191. * Can be overridden by a Migration subclass if one would like to push the
  192. * envelope. Defaults to 90%.
  193. *
  194. * @var float
  195. */
  196. protected $timeThreshold = 0.90;
  197. /**
  198. * The PHP max_execution_time.
  199. *
  200. * @var int
  201. */
  202. protected $timeLimit;
  203. /**
  204. * A time limit in seconds appropriate to be used in a batch
  205. * import. Defaults to 240.
  206. *
  207. * @var int
  208. */
  209. protected $batchTimeLimit = 240;
  210. /**
  211. * MigrateTeamMember objects representing people involved with this
  212. * migration.
  213. *
  214. * @var array
  215. */
  216. protected $team = array();
  217. public function getTeam() {
  218. return $this->team;
  219. }
  220. public function setTeam(array $team) {
  221. $this->team = $team;
  222. }
  223. /**
  224. * If provided, an URL for an issue tracking system containing :id where
  225. * the issue number will go (e.g., 'http://example.com/project/ticket/:id').
  226. *
  227. * @var string
  228. */
  229. protected $issuePattern;
  230. public function getIssuePattern() {
  231. return $this->issuePattern;
  232. }
  233. public function setIssuePattern($issue_pattern) {
  234. $this->issuePattern = $issue_pattern;
  235. }
  236. /**
  237. * If we set an error handler (during import), remember the previous one so
  238. * it can be restored.
  239. *
  240. * @var callback
  241. */
  242. protected $previousErrorHandler = NULL;
  243. /**
  244. * Arguments configuring a migration.
  245. *
  246. * @var array
  247. */
  248. protected $arguments = array();
  249. public function getArguments() {
  250. return $this->arguments;
  251. }
  252. public function setArguments(array $arguments) {
  253. $this->arguments = $arguments;
  254. }
  255. public function addArguments(array $arguments) {
  256. $this->arguments = array_merge($this->arguments, $arguments);
  257. }
  258. /**
  259. * Disabling a migration prevents it from running with --all, or individually
  260. * without --force
  261. *
  262. * @var boolean
  263. */
  264. protected $enabled = TRUE;
  265. public function getEnabled() {
  266. return $this->enabled;
  267. }
  268. public function setEnabled($enabled) {
  269. $this->enabled = $enabled;
  270. }
  271. /**
  272. * Any module hooks which should be disabled during migration processes.
  273. *
  274. * @var array
  275. * Key: Hook name (e.g., 'node_insert')
  276. * Value: Array of modules for which to disable this hook (e.g.,
  277. * array('pathauto')).
  278. */
  279. protected $disableHooks = array();
  280. public function getDisableHooks() {
  281. return $this->disableHooks;
  282. }
  283. /**
  284. * An array to track 'mail_system' variable if disabled.
  285. */
  286. protected $mailSystem;
  287. /**
  288. * Have we already warned about obsolete constructor argumentss on this
  289. * request?
  290. *
  291. * @var bool
  292. */
  293. static protected $groupArgumentWarning = FALSE;
  294. static protected $emptyArgumentsWarning = FALSE;
  295. /**
  296. * Codes representing the result of a rollback or import process.
  297. */
  298. const RESULT_COMPLETED = 1; // All records have been processed
  299. const RESULT_INCOMPLETE = 2; // The process has interrupted itself (e.g., the
  300. // memory limit is approaching)
  301. const RESULT_STOPPED = 3; // The process was stopped externally (e.g., via
  302. // drush migrate-stop)
  303. const RESULT_FAILED = 4; // The process had a fatal error
  304. const RESULT_SKIPPED = 5; // Dependencies are unfulfilled - skip the process
  305. const RESULT_DISABLED = 6; // This migration is disabled, skipping
  306. /**
  307. * Codes representing the current status of a migration, and stored in the
  308. * migrate_status table.
  309. */
  310. const STATUS_IDLE = 0;
  311. const STATUS_IMPORTING = 1;
  312. const STATUS_ROLLING_BACK = 2;
  313. const STATUS_STOPPING = 3;
  314. const STATUS_DISABLED = 4;
  315. /**
  316. * Message types to be passed to saveMessage() and saved in message tables.
  317. * MESSAGE_INFORMATIONAL represents a condition that did not prevent the
  318. * operation from succeeding - all others represent different severities of
  319. * conditions resulting in a source record not being imported.
  320. */
  321. const MESSAGE_ERROR = 1;
  322. const MESSAGE_WARNING = 2;
  323. const MESSAGE_NOTICE = 3;
  324. const MESSAGE_INFORMATIONAL = 4;
  325. /**
  326. * Get human readable name for a message constant.
  327. *
  328. * @return string
  329. * Name.
  330. */
  331. public function getMessageLevelName($constant) {
  332. $map = array(
  333. MigrationBase::MESSAGE_ERROR => t('Error'),
  334. MigrationBase::MESSAGE_WARNING => t('Warning'),
  335. MigrationBase::MESSAGE_NOTICE => t('Notice'),
  336. MigrationBase::MESSAGE_INFORMATIONAL => t('Informational'),
  337. );
  338. return $map[$constant];
  339. }
  340. /**
  341. * Construction of a MigrationBase instance.
  342. *
  343. * @param array $arguments
  344. */
  345. public function __construct($arguments = array()) {
  346. // Support for legacy code passing a group object as the first parameter.
  347. if (is_object($arguments) && is_a($arguments, 'MigrateGroup')) {
  348. $this->group = $arguments;
  349. $this->arguments['group_name'] = $arguments->getName();
  350. if (!self::$groupArgumentWarning &&
  351. variable_get('migrate_deprecation_warnings', 1)) {
  352. self::displayMessage(t('Passing a group object to a migration constructor is now deprecated - pass through the arguments array passed to the leaf class instead.'));
  353. self::$groupArgumentWarning = TRUE;
  354. }
  355. }
  356. else {
  357. if (empty($arguments)) {
  358. $this->arguments = array();
  359. if (!self::$emptyArgumentsWarning &&
  360. variable_get('migrate_deprecation_warnings', 1)) {
  361. self::displayMessage(t('Passing an empty first parameter to a migration constructor is now deprecated - pass through the arguments array passed to the leaf class instead.'));
  362. self::$emptyArgumentsWarning = TRUE;
  363. }
  364. }
  365. else {
  366. $this->arguments = $arguments;
  367. }
  368. if (empty($this->arguments['group_name'])) {
  369. $this->arguments['group_name'] = 'default';
  370. }
  371. $this->group = MigrateGroup::getInstance($this->arguments['group_name']);
  372. }
  373. if (isset($this->arguments['machine_name'])) {
  374. $this->machineName = $this->arguments['machine_name'];
  375. }
  376. else {
  377. // Deprecated - this supports old code which does not pass the arguments
  378. // array through to the base constructor. Remove in the next version.
  379. $this->machineName = $this->machineFromClass(get_class($this));
  380. }
  381. // Make any group arguments directly accessible to the specific migration,
  382. // other than group dependencies.
  383. $group_arguments = $this->group->getArguments();
  384. unset($group_arguments['dependencies']);
  385. $this->arguments += $group_arguments;
  386. // Record the memory limit in bytes
  387. $limit = trim(ini_get('memory_limit'));
  388. if ($limit == '-1') {
  389. $this->memoryLimit = PHP_INT_MAX;
  390. }
  391. else {
  392. if (!is_numeric($limit)) {
  393. $last = drupal_strtolower($limit[strlen($limit) - 1]);
  394. $limit = substr($limit, 0, -1);
  395. switch ($last) {
  396. case 'g':
  397. $limit *= 1024;
  398. case 'm':
  399. $limit *= 1024;
  400. case 'k':
  401. $limit *= 1024;
  402. break;
  403. default:
  404. throw new Exception(t('Invalid PHP memory_limit !limit',
  405. array('!limit' => $limit)));
  406. }
  407. }
  408. $this->memoryLimit = $limit;
  409. }
  410. // Record the time limit
  411. $this->timeLimit = ini_get('max_execution_time');
  412. // Make sure we clear our semaphores in case of abrupt exit
  413. drupal_register_shutdown_function(array($this, 'endProcess'));
  414. // Save any hook disablement information.
  415. if (isset($this->arguments['disable_hooks']) &&
  416. is_array($this->arguments['disable_hooks'])) {
  417. $this->disableHooks = $this->arguments['disable_hooks'];
  418. }
  419. }
  420. /**
  421. * Initialize static members, before any class instances are created.
  422. */
  423. static public function staticInitialize() {
  424. // Default the displayFunction outputFunction based on context
  425. if (function_exists('drush_log')) {
  426. self::$displayFunction = 'drush_log';
  427. }
  428. else {
  429. self::$displayFunction = 'drupal_set_message';
  430. }
  431. }
  432. /**
  433. * Register a new migration process in the migrate_status table. This will
  434. * generally be used in two contexts - by the class detection code for
  435. * static (one instance per class) migrations, and by the module implementing
  436. * dynamic (parameterized class) migrations.
  437. *
  438. * @param string $class_name
  439. * @param string $machine_name
  440. * @param array $arguments
  441. */
  442. static public function registerMigration($class_name, $machine_name = NULL,
  443. array $arguments = array()) {
  444. // Support for legacy migration code - in later releases, the machine_name
  445. // should always be explicit.
  446. if (!$machine_name) {
  447. $machine_name = self::machineFromClass($class_name);
  448. }
  449. if (!preg_match('|^[a-z0-9_]+$|i', $machine_name)) {
  450. throw new Exception(t('!name is not a valid Migration machine name. Use only alphanumeric or underscore characters.',
  451. array('!name' => $machine_name)));
  452. }
  453. // We no longer have any need to store the machine_name in the arguments.
  454. if (isset($arguments['machine_name'])) {
  455. unset($arguments['machine_name']);
  456. }
  457. if (isset($arguments['group_name'])) {
  458. $group_name = $arguments['group_name'];
  459. unset($arguments['group_name']);
  460. }
  461. else {
  462. $group_name = 'default';
  463. }
  464. $arguments = self::encryptArguments($arguments);
  465. // Register the migration if it's not already there; if it is,
  466. // update the class and arguments in case they've changed.
  467. db_merge('migrate_status')
  468. ->key(array('machine_name' => $machine_name))
  469. ->fields(array(
  470. 'class_name' => $class_name,
  471. 'group_name' => $group_name,
  472. 'arguments' => serialize($arguments),
  473. ))
  474. ->execute();
  475. }
  476. /**
  477. * Deregister a migration - remove all traces of it from the database (without
  478. * touching any content which was created by this migration).
  479. *
  480. * @param string $machine_name
  481. */
  482. static public function deregisterMigration($machine_name) {
  483. $rows_deleted = db_delete('migrate_status')
  484. ->condition('machine_name', $machine_name)
  485. ->execute();
  486. // Make sure the group gets deleted if we were the only member.
  487. MigrateGroup::deleteOrphans();
  488. }
  489. /**
  490. * The migration machine name is stored in the arguments.
  491. *
  492. * @return string
  493. */
  494. protected function generateMachineName() {
  495. return $this->arguments['machine_name'];
  496. }
  497. /**
  498. * Given only a class name, derive a machine name (the class name with the
  499. * "Migration" suffix, if any, removed).
  500. *
  501. * @param $class_name
  502. *
  503. * @return string
  504. */
  505. protected static function machineFromClass($class_name) {
  506. if (preg_match('/Migration$/', $class_name)) {
  507. $machine_name = drupal_substr($class_name, 0,
  508. strlen($class_name) - strlen('Migration'));
  509. }
  510. else {
  511. $machine_name = $class_name;
  512. }
  513. return $machine_name;
  514. }
  515. /**
  516. * Return the single instance of the given migration.
  517. *
  518. * @param string $machine_name
  519. */
  520. /**
  521. * Return the single instance of the given migration.
  522. *
  523. * @param $machine_name
  524. * The unique machine name of the migration to retrieve.
  525. * @param string $class_name
  526. * Deprecated - no longer used, class name is retrieved from migrate_status.
  527. * @param array $arguments
  528. * Deprecated - no longer used, arguments are retrieved from migrate_status.
  529. *
  530. * @return MigrationBase
  531. */
  532. static public function getInstance($machine_name, $class_name = NULL, array $arguments = array()) {
  533. $migrations = &drupal_static(__FUNCTION__, array());
  534. // Otherwise might miss cache hit on case difference
  535. $machine_name_key = drupal_strtolower($machine_name);
  536. if (!isset($migrations[$machine_name_key])) {
  537. // See if we know about this migration
  538. $row = db_select('migrate_status', 'ms')
  539. ->fields('ms', array('class_name', 'group_name', 'arguments'))
  540. ->condition('machine_name', $machine_name)
  541. ->execute()
  542. ->fetchObject();
  543. if ($row) {
  544. $class_name = $row->class_name;
  545. $arguments = unserialize($row->arguments);
  546. $arguments = self::decryptArguments($arguments);
  547. $arguments['group_name'] = $row->group_name;
  548. }
  549. else {
  550. // Can't find a migration with this name
  551. self::displayMessage(t('No migration found with machine name !machine',
  552. array('!machine' => $machine_name)));
  553. return NULL;
  554. }
  555. $arguments['machine_name'] = $machine_name;
  556. if (class_exists($class_name)) {
  557. try {
  558. $migrations[$machine_name_key] = new $class_name($arguments);
  559. } catch (Exception $e) {
  560. self::displayMessage(t('Migration !machine could not be constructed.',
  561. array('!machine' => $machine_name)));
  562. self::displayMessage($e->getMessage());
  563. return NULL;
  564. }
  565. }
  566. else {
  567. self::displayMessage(t('No migration class !class found',
  568. array('!class' => $class_name)));
  569. return NULL;
  570. }
  571. if (isset($arguments['dependencies'])) {
  572. $migrations[$machine_name_key]->setHardDependencies(
  573. $arguments['dependencies']);
  574. }
  575. if (isset($arguments['soft_dependencies'])) {
  576. $migrations[$machine_name_key]->setSoftDependencies(
  577. $arguments['soft_dependencies']);
  578. }
  579. }
  580. return $migrations[$machine_name_key];
  581. }
  582. /**
  583. * @deprecated - No longer a useful distinction between "status" and "dynamic"
  584. * migrations.
  585. */
  586. static public function isDynamic() {
  587. return FALSE;
  588. }
  589. /**
  590. * Default to printing messages, but derived classes are expected to save
  591. * messages indexed by current source ID.
  592. *
  593. * @param string $message
  594. * The message to record.
  595. * @param int $level
  596. * Optional message severity (defaults to MESSAGE_ERROR).
  597. */
  598. public function saveMessage($message, $level = MigrationBase::MESSAGE_ERROR) {
  599. switch ($level) {
  600. case MigrationBase::MESSAGE_ERROR:
  601. $level = 'error';
  602. break;
  603. case MigrationBase::MESSAGE_WARNING:
  604. $level = 'warning';
  605. break;
  606. case MigrationBase::MESSAGE_NOTICE:
  607. $level = 'notice';
  608. break;
  609. case MigrationBase::MESSAGE_INFORMATIONAL:
  610. $level = 'status';
  611. break;
  612. }
  613. self::displayMessage($message, $level);
  614. }
  615. /**
  616. * Output the given message appropriately
  617. * (drush_print/drupal_set_message/etc.)
  618. *
  619. * @param string $message
  620. * The message to output.
  621. * @param int $level
  622. * Optional message severity as understood by drupal_set_message and
  623. * drush_log
  624. * (defaults to 'error').
  625. */
  626. static public function displayMessage($message, $level = 'error') {
  627. call_user_func(self::$displayFunction, $message, $level);
  628. }
  629. /**
  630. * Custom PHP error handler.
  631. * TODO: Redundant with hook_watchdog?
  632. *
  633. * @param $error_level
  634. * The level of the error raised.
  635. * @param $message
  636. * The error message.
  637. * @param $filename
  638. * The filename that the error was raised in.
  639. * @param $line
  640. * The line number the error was raised at.
  641. * @param $context
  642. * An array that points to the active symbol table at the point the error
  643. * occurred.
  644. */
  645. public function errorHandler($error_level, $message, $filename, $line, $context) {
  646. if ($error_level & error_reporting()) {
  647. $message .= "\n" . t('File !file, line !line',
  648. array('!line' => $line, '!file' => $filename));
  649. // Record notices and continue
  650. if ($error_level == E_NOTICE || $error_level == E_USER_NOTICE) {
  651. $this->saveMessage($message . "(file: $filename, line $line)", MigrationBase::MESSAGE_INFORMATIONAL);
  652. }
  653. // Simply ignore strict and deprecated errors
  654. // Note DEPRECATED constants introduced in PHP 5.3
  655. elseif (!($error_level == E_STRICT || $error_level == 8192 ||
  656. $error_level == 16384)) {
  657. throw new MigrateException($message, MigrationBase::MESSAGE_ERROR);
  658. }
  659. }
  660. }
  661. /**
  662. * Takes an Exception object and both saves and displays it, pulling
  663. * additional information on the location triggering the exception.
  664. *
  665. * @param Exception $exception
  666. * Object representing the exception.
  667. * @param boolean $save
  668. * Whether to save the message in the migration's mapping table. Set to
  669. * FALSE
  670. * in contexts where this doesn't make sense.
  671. */
  672. public function handleException($exception, $save = TRUE) {
  673. $result = _drupal_decode_exception($exception);
  674. $message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')';
  675. if ($save) {
  676. $this->saveMessage($message);
  677. }
  678. self::displayMessage($message);
  679. }
  680. /**
  681. * Check the current status of a migration.
  682. *
  683. * @return int
  684. * One of the MigrationBase::STATUS_* constants
  685. */
  686. public function getStatus() {
  687. if (!$this->enabled) {
  688. return MigrationBase::STATUS_DISABLED;
  689. }
  690. $status = db_select('migrate_status', 'ms')
  691. ->fields('ms', array('status'))
  692. ->condition('machine_name', $this->machineName)
  693. ->execute()
  694. ->fetchField();
  695. if (!isset($status)) {
  696. $status = MigrationBase::STATUS_IDLE;
  697. }
  698. return $status;
  699. }
  700. /**
  701. * Retrieve the last time an import operation completed successfully.
  702. *
  703. * @return string
  704. * Date/time string, formatted... How? Default DB server format?
  705. */
  706. public function getLastImported() {
  707. $last_imported = db_select('migrate_log', 'ml')
  708. ->fields('ml', array('endtime'))
  709. ->condition('machine_name', $this->machineName)
  710. ->isNotNull('endtime')
  711. ->orderBy('endtime', 'DESC')
  712. ->execute()
  713. ->fetchField();
  714. if ($last_imported) {
  715. $last_imported = date('Y-m-d H:i:s', $last_imported / 1000);
  716. }
  717. else {
  718. $last_imported = '';
  719. }
  720. return $last_imported;
  721. }
  722. /**
  723. * Fetch the current highwater mark for updated content.
  724. *
  725. * @return string
  726. * The highwater mark.
  727. */
  728. public function getHighwater() {
  729. $highwater = db_select('migrate_status', 'ms')
  730. ->fields('ms', array('highwater'))
  731. ->condition('machine_name', $this->machineName)
  732. ->execute()
  733. ->fetchField();
  734. return $highwater;
  735. }
  736. /**
  737. * Save the highwater mark for this migration (but not when using an idlist).
  738. *
  739. * @param mixed $highwater
  740. * Highwater mark to save
  741. * @param boolean $force
  742. * If TRUE, save even if it's lower than the previous value.
  743. */
  744. protected function saveHighwater($highwater, $force = FALSE) {
  745. if (!isset($this->options['idlist'])) {
  746. $query = db_update('migrate_status')
  747. ->fields(array('highwater' => $highwater))
  748. ->condition('machine_name', $this->machineName);
  749. if (!$force) {
  750. if (!empty($this->highwaterField['type']) && $this->highwaterField['type'] == 'int') {
  751. // If the highwater is an integer type, we need to force the DB server
  752. // to treat the varchar highwater field as an integer (otherwise it will
  753. // think '5' > '10').
  754. switch (Database::getConnection()->databaseType()) {
  755. case 'pgsql':
  756. $query->where('(CASE WHEN highwater=\'\' THEN 0 ELSE CAST(highwater AS INTEGER) END) < :highwater', array(':highwater' => intval($highwater)));
  757. break;
  758. default:
  759. // MySQL casts as integers as SIGNED or UNSIGNED.
  760. $query->where('(CASE WHEN highwater=\'\' THEN 0 ELSE CAST(highwater AS SIGNED) END) < :highwater', array(':highwater' => intval($highwater)));
  761. }
  762. }
  763. else {
  764. $query->condition('highwater', $highwater, '<');
  765. }
  766. }
  767. $query->execute();
  768. }
  769. }
  770. /**
  771. * Retrieve the last throughput for current Migration (items / minute).
  772. *
  773. * @return integer
  774. */
  775. public function getLastThroughput() {
  776. $last_throughput = 0;
  777. $row = db_select('migrate_log', 'ml')
  778. ->fields('ml', array('starttime', 'endtime', 'numprocessed'))
  779. ->condition('machine_name', $this->machineName)
  780. ->condition('process_type', 1)
  781. ->isNotNull('endtime')
  782. ->orderBy('starttime', 'DESC')
  783. ->execute()
  784. ->fetchObject();
  785. if ($row) {
  786. $elapsed = ($row->endtime - $row->starttime) / 1000;
  787. if ($elapsed > 0) {
  788. $last_throughput = round(($row->numprocessed / $elapsed) * 60);
  789. }
  790. }
  791. return $last_throughput;
  792. }
  793. /**
  794. * Reports whether this migration process is complete. For a Migration, for
  795. * example, this would be whether all available source rows have been
  796. * processed. Other MigrationBase classes will need to return TRUE/FALSE
  797. * appropriately.
  798. */
  799. abstract public function isComplete();
  800. /**
  801. * Reports whether all (hard) dependencies have completed migration
  802. */
  803. protected function dependenciesComplete($rollback = FALSE) {
  804. if ($rollback) {
  805. foreach (migrate_migrations() as $migration) {
  806. $dependencies = $migration->getHardDependencies();
  807. if (array_search($this->machineName, $dependencies) !== FALSE) {
  808. if (method_exists($migration, 'importedCount') && $migration->importedCount() > 0) {
  809. return FALSE;
  810. }
  811. }
  812. }
  813. }
  814. else {
  815. foreach ($this->dependencies as $dependency) {
  816. $migration = MigrationBase::getInstance($dependency);
  817. if (!$migration || !$migration->isComplete()) {
  818. return FALSE;
  819. }
  820. }
  821. }
  822. return TRUE;
  823. }
  824. /**
  825. * Returns an array of the migration's dependencies that are incomplete.
  826. */
  827. public function incompleteDependencies() {
  828. $incomplete = array();
  829. foreach ($this->getDependencies() as $dependency) {
  830. $migration = MigrationBase::getInstance($dependency);
  831. if (!$migration || !$migration->isComplete()) {
  832. $incomplete[] = $dependency;
  833. }
  834. }
  835. return $incomplete;
  836. }
  837. /**
  838. * Begin a process, ensuring only one process can be active
  839. * at once on a given migration.
  840. *
  841. * @param int $newStatus
  842. * MigrationBase::STATUS_IMPORTING or MigrationBase::STATUS_ROLLING_BACK
  843. */
  844. protected function beginProcess($newStatus) {
  845. // So hook_watchdog() knows what migration (if any) is running
  846. self::$currentMigration = $this;
  847. // Try to make the semaphore handling atomic (depends on DB support)
  848. $transaction = db_transaction();
  849. // Save the current mail system, prior to disabling emails.
  850. $this->saveMailSystem();
  851. // Prevent emails from being sent out during migrations.
  852. $this->disableMailSystem();
  853. $this->starttime = microtime(TRUE);
  854. // Check to make sure there's no process already running for this migration
  855. $status = $this->getStatus();
  856. if ($status != MigrationBase::STATUS_IDLE) {
  857. throw new MigrateException(t('There is already an active process on !machine_name',
  858. array('!machine_name' => $this->machineName)));
  859. }
  860. $this->processing = TRUE;
  861. $this->status = $newStatus;
  862. db_merge('migrate_status')
  863. ->key(array('machine_name' => $this->machineName))
  864. ->fields(array('class_name' => get_class($this), 'status' => $newStatus))
  865. ->execute();
  866. // Set an error handler for imports
  867. if ($newStatus == MigrationBase::STATUS_IMPORTING) {
  868. $this->previousErrorHandler = set_error_handler(array(
  869. $this,
  870. 'errorHandler',
  871. ));
  872. }
  873. // Save the initial history record
  874. if ($this->logHistory) {
  875. $this->logID = db_insert('migrate_log')
  876. ->fields(array(
  877. 'machine_name' => $this->machineName,
  878. 'process_type' => $newStatus,
  879. 'starttime' => round(microtime(TRUE) * 1000),
  880. 'initialHighwater' => $this->getHighwater(),
  881. ))
  882. ->execute();
  883. }
  884. // If we're disabling any hooks, reset the static module_implements cache so
  885. // it is rebuilt with the specified hooks removed by our
  886. // hook_module_implements_alter(). By setting #write_cache to FALSE, we
  887. // ensure that our munged version of the hooks array does not get written
  888. // to the persistent cache and interfere with other Drupal processes.
  889. if (!empty($this->disableHooks)) {
  890. $implementations = &drupal_static('module_implements');
  891. $implementations = array();
  892. $implementations['#write_cache'] = FALSE;
  893. }
  894. }
  895. /**
  896. * End a rollback or import process, releasing the semaphore. Note that it
  897. * must be public to be callable as the shutdown function.
  898. */
  899. public function endProcess() {
  900. if ($this->previousErrorHandler) {
  901. set_error_handler($this->previousErrorHandler);
  902. $this->previousErrorHandler = NULL;
  903. }
  904. if ($this->processing) {
  905. $this->status = MigrationBase::STATUS_IDLE;
  906. // Restore the previous mail handler.
  907. $this->restoreMailSystem();
  908. $fields = array(
  909. 'class_name' => get_class($this),
  910. 'status' => MigrationBase::STATUS_IDLE,
  911. );
  912. db_merge('migrate_status')
  913. ->key(array('machine_name' => $this->machineName))
  914. ->fields($fields)
  915. ->execute();
  916. // Complete the log record
  917. if ($this->logHistory) {
  918. try {
  919. db_merge('migrate_log')
  920. ->key(array('mlid' => $this->logID))
  921. ->fields(array(
  922. 'endtime' => round(microtime(TRUE) * 1000),
  923. 'finalhighwater' => $this->getHighwater(),
  924. 'numprocessed' => $this->total_processed,
  925. ))
  926. ->execute();
  927. } catch (PDOException $e) {
  928. Migration::displayMessage(t('Could not log operation on migration !name - possibly MigrationBase::beginProcess() was not called',
  929. array('!name' => $this->machineName)));
  930. }
  931. }
  932. $this->processing = FALSE;
  933. }
  934. self::$currentMigration = NULL;
  935. }
  936. /**
  937. * Signal that any current import or rollback process should end itself at
  938. * the earliest opportunity
  939. */
  940. public function stopProcess() {
  941. // Do not change the status of an idle migration
  942. db_update('migrate_status')
  943. ->fields(array('status' => MigrationBase::STATUS_STOPPING))
  944. ->condition('machine_name', $this->machineName)
  945. ->condition('status', MigrationBase::STATUS_IDLE, '<>')
  946. ->execute();
  947. }
  948. /**
  949. * Reset the status of the migration to IDLE (to be used when the status
  950. * gets stuck, e.g. if a process core-dumped)
  951. */
  952. public function resetStatus() {
  953. // Do not change the status of an already-idle migration
  954. db_update('migrate_status')
  955. ->fields(array('status' => MigrationBase::STATUS_IDLE))
  956. ->condition('machine_name', $this->machineName)
  957. ->condition('status', MigrationBase::STATUS_IDLE, '<>')
  958. ->execute();
  959. }
  960. /**
  961. * Perform an operation during the rollback phase.
  962. *
  963. * @param array $options
  964. * List of options provided (usually from a drush command). Specific to
  965. * the derived class.
  966. */
  967. public function processRollback(array $options = array()) {
  968. if ($this->enabled) {
  969. $return = MigrationBase::RESULT_COMPLETED;
  970. if (method_exists($this, 'rollback')) {
  971. $this->options = $options;
  972. if (!isset($options['force'])) {
  973. if (!$this->dependenciesComplete(TRUE)) {
  974. return MigrationBase::RESULT_SKIPPED;
  975. }
  976. }
  977. $this->beginProcess(MigrationBase::STATUS_ROLLING_BACK);
  978. try {
  979. $return = $this->rollback();
  980. } catch (Exception $exception) {
  981. // If something bad happened, make sure we clear the semaphore
  982. $this->endProcess();
  983. throw $exception;
  984. }
  985. $this->endProcess();
  986. }
  987. }
  988. else {
  989. $return = MigrationBase::RESULT_DISABLED;
  990. }
  991. return $return;
  992. }
  993. /**
  994. * Perform an operation during the import phase
  995. *
  996. * @param array $options
  997. * List of options provided (usually from a drush command). Specific to
  998. * the derived class.
  999. */
  1000. public function processImport(array $options = array()) {
  1001. if ($this->enabled) {
  1002. $return = MigrationBase::RESULT_COMPLETED;
  1003. if (method_exists($this, 'import')) {
  1004. $this->options = $options;
  1005. if (!isset($options['force']) || !$options['force']) {
  1006. if (!$this->dependenciesComplete()) {
  1007. return MigrationBase::RESULT_SKIPPED;
  1008. }
  1009. }
  1010. $this->beginProcess(MigrationBase::STATUS_IMPORTING);
  1011. try {
  1012. $return = $this->import();
  1013. } catch (Exception $exception) {
  1014. // If something bad happened, make sure we clear the semaphore
  1015. $this->endProcess();
  1016. throw $exception;
  1017. }
  1018. if ($return == MigrationBase::RESULT_COMPLETED && isset($this->total_successes)) {
  1019. $time = microtime(TRUE) - $this->starttime;
  1020. if ($time > 0) {
  1021. $overallThroughput = round(60 * $this->total_successes / $time);
  1022. }
  1023. else {
  1024. $overallThroughput = 9999;
  1025. }
  1026. }
  1027. else {
  1028. $overallThroughput = 0;
  1029. }
  1030. $this->endProcess($overallThroughput);
  1031. }
  1032. }
  1033. else {
  1034. $return = MigrationBase::RESULT_DISABLED;
  1035. }
  1036. return $return;
  1037. }
  1038. /**
  1039. * Set the PHP time limit. This method may be called from batch callbacks
  1040. * before calling the processImport method.
  1041. */
  1042. public function setBatchTimeLimit() {
  1043. drupal_set_time_limit($this->batchTimeLimit);
  1044. }
  1045. /**
  1046. * A derived migration class does the actual rollback or import work in these
  1047. * methods - we cannot declare them abstract because some classes may define
  1048. * only one.
  1049. *
  1050. * abstract protected function rollback();
  1051. * abstract protected function import();
  1052. */
  1053. /**
  1054. * Test whether we've exceeded the desired memory threshold. If so, output a
  1055. * message.
  1056. *
  1057. * @return boolean
  1058. * TRUE if the threshold is exceeded, FALSE if not.
  1059. */
  1060. protected function memoryExceeded() {
  1061. $usage = memory_get_usage();
  1062. $pct_memory = $usage / $this->memoryLimit;
  1063. if ($pct_memory > $this->memoryThreshold) {
  1064. self::displayMessage(
  1065. t('Memory usage is !usage (!pct% of limit !limit), resetting statics',
  1066. array(
  1067. '!pct' => round($pct_memory * 100),
  1068. '!usage' => format_size($usage),
  1069. '!limit' => format_size($this->memoryLimit),
  1070. )),
  1071. 'warning');
  1072. // First, try resetting Drupal's static storage - this frequently releases
  1073. // plenty of memory to continue
  1074. drupal_static_reset();
  1075. $usage = memory_get_usage();
  1076. $pct_memory = $usage / $this->memoryLimit;
  1077. // Use a lower threshold - we don't want to be in a situation where we keep
  1078. // coming back here and trimming a tiny amount
  1079. if ($pct_memory > (.90 * $this->memoryThreshold)) {
  1080. self::displayMessage(
  1081. t('Memory usage is now !usage (!pct% of limit !limit), not enough reclaimed, starting new batch',
  1082. array(
  1083. '!pct' => round($pct_memory * 100),
  1084. '!usage' => format_size($usage),
  1085. '!limit' => format_size($this->memoryLimit),
  1086. )),
  1087. 'warning');
  1088. return TRUE;
  1089. }
  1090. else {
  1091. self::displayMessage(
  1092. t('Memory usage is now !usage (!pct% of limit !limit), reclaimed enough, continuing',
  1093. array(
  1094. '!pct' => round($pct_memory * 100),
  1095. '!usage' => format_size($usage),
  1096. '!limit' => format_size($this->memoryLimit),
  1097. )),
  1098. 'warning');
  1099. return FALSE;
  1100. }
  1101. }
  1102. else {
  1103. return FALSE;
  1104. }
  1105. }
  1106. /**
  1107. * Test whether we're approaching the PHP time limit.
  1108. *
  1109. * @return boolean
  1110. * TRUE if the threshold is exceeded, FALSE if not.
  1111. */
  1112. protected function timeExceeded() {
  1113. if ($this->timeLimit == 0) {
  1114. return FALSE;
  1115. }
  1116. $time_elapsed = time() - REQUEST_TIME;
  1117. $pct_time = $time_elapsed / $this->timeLimit;
  1118. if ($pct_time > $this->timeThreshold) {
  1119. return TRUE;
  1120. }
  1121. else {
  1122. return FALSE;
  1123. }
  1124. }
  1125. /**
  1126. * Test whether we've exceeded the designated time limit.
  1127. *
  1128. * @return boolean
  1129. * TRUE if the threshold is exceeded, FALSE if not.
  1130. */
  1131. protected function timeOptionExceeded() {
  1132. if (!$timelimit = $this->getTimeLimit()) {
  1133. return FALSE;
  1134. }
  1135. $time_elapsed = time() - REQUEST_TIME;
  1136. if ($time_elapsed >= $timelimit) {
  1137. return TRUE;
  1138. }
  1139. else {
  1140. return FALSE;
  1141. }
  1142. }
  1143. /**
  1144. * Encrypt an incoming value. Detects for existence of the Drupal 'Encrypt'
  1145. * module.
  1146. *
  1147. * @param string $value
  1148. *
  1149. * @return string The encrypted value.
  1150. */
  1151. static public function encrypt($value) {
  1152. if (module_exists('encrypt')) {
  1153. $value = encrypt($value);
  1154. }
  1155. else {
  1156. if (self::$showEncryptionWarning) {
  1157. MigrationBase::displayMessage(t('Encryption of secure migration information is not supported. Ensure the <a href="@encrypt">Encrypt module</a> is installed for this functionality.',
  1158. array(
  1159. '@encrypt' => 'http://drupal.org/project/encrypt',
  1160. )
  1161. ),
  1162. 'warning');
  1163. self::$showEncryptionWarning = FALSE;
  1164. }
  1165. }
  1166. return $value;
  1167. }
  1168. /**
  1169. * Decrypt an incoming value.
  1170. *
  1171. * @param string $value
  1172. *
  1173. * @return string The encrypted value
  1174. */
  1175. static public function decrypt($value) {
  1176. if (module_exists('encrypt')) {
  1177. $value = decrypt($value);
  1178. }
  1179. else {
  1180. if (self::$showEncryptionWarning) {
  1181. MigrationBase::displayMessage(t('Encryption of secure migration information is not supported. Ensure the <a href="@encrypt">Encrypt module</a> is installed for this functionality.',
  1182. array(
  1183. '@encrypt' => 'http://drupal.org/project/encrypt',
  1184. )
  1185. ),
  1186. 'warning');
  1187. self::$showEncryptionWarning = FALSE;
  1188. }
  1189. }
  1190. return $value;
  1191. }
  1192. /**
  1193. * Make sure any arguments we want to be encrypted get encrypted.
  1194. *
  1195. * @param array $arguments
  1196. *
  1197. * @return array
  1198. */
  1199. static public function encryptArguments(array $arguments) {
  1200. if (isset($arguments['encrypted_arguments'])) {
  1201. foreach ($arguments['encrypted_arguments'] as $argument_name) {
  1202. if (isset($arguments[$argument_name])) {
  1203. $arguments[$argument_name] = self::encrypt(
  1204. serialize($arguments[$argument_name]));
  1205. }
  1206. }
  1207. }
  1208. return $arguments;
  1209. }
  1210. /**
  1211. * Make sure any arguments we want to be decrypted get decrypted.
  1212. *
  1213. * @param array $arguments
  1214. *
  1215. * @return array
  1216. */
  1217. static public function decryptArguments(array $arguments) {
  1218. if (isset($arguments['encrypted_arguments'])) {
  1219. foreach ($arguments['encrypted_arguments'] as $argument_name) {
  1220. if (isset($arguments[$argument_name])) {
  1221. $decrypted_string = self::decrypt($arguments[$argument_name]);
  1222. // A decryption failure will return FALSE and issue a notice. We need
  1223. // to distinguish a failure from a serialized FALSE.
  1224. $unserialized_value = @unserialize($decrypted_string);
  1225. if ($unserialized_value === FALSE && $decrypted_string != serialize(FALSE)) {
  1226. self::displayMessage(t('Failed to decrypt argument %argument_name',
  1227. array('%argument_name' => $argument_name)));
  1228. unset($arguments[$argument_name]);
  1229. }
  1230. else {
  1231. $arguments[$argument_name] = $unserialized_value;
  1232. }
  1233. }
  1234. }
  1235. }
  1236. return $arguments;
  1237. }
  1238. /**
  1239. * Convert an incoming string (which may be a UNIX timestamp, or an
  1240. * arbitrarily-formatted date/time string) to a UNIX timestamp.
  1241. *
  1242. * @param string $value
  1243. * The time string to convert.
  1244. * @param string $timezone
  1245. * Optional timezone for the time string. NULL to leave the timezone unset.
  1246. *
  1247. * @return string
  1248. * The UNIX timestamp.
  1249. */
  1250. static public function timestamp($value, $timezone = NULL) {
  1251. // Does it look like it's already a timestamp? Just return it
  1252. if (is_numeric($value)) {
  1253. return $value;
  1254. }
  1255. // Default empty values to now
  1256. if (empty($value)) {
  1257. return time();
  1258. }
  1259. if (isset($timezone)) {
  1260. $timezone = new DateTimeZone($timezone);
  1261. }
  1262. $date = new DateTime($value, $timezone);
  1263. $time = $date->format('U');
  1264. if ($time == FALSE) {
  1265. // Handles form YYYY-MM-DD HH:MM:SS.garbage
  1266. if (drupal_strlen($value) > 19) {
  1267. $time = strtotime(drupal_substr($value, 0, 19));
  1268. }
  1269. }
  1270. return $time;
  1271. }
  1272. /**
  1273. * Saves the current mail system, or set a system default if there is none.
  1274. */
  1275. public function saveMailSystem() {
  1276. global $conf;
  1277. $this->mailSystem = empty($conf['mail_system']) ? NULL : $conf['mail_system'];
  1278. }
  1279. /**
  1280. * Disables mail system to prevent emails from being sent during migrations.
  1281. */
  1282. public function disableMailSystem() {
  1283. global $conf;
  1284. if (!empty($conf['mail_system'])) {
  1285. foreach ($conf['mail_system'] as $system => $class) {
  1286. $conf['mail_system'][$system] = 'MigrateMailIgnore';
  1287. }
  1288. }
  1289. else {
  1290. $conf['mail_system'] = array('default-system' => 'MigrateMailIgnore');
  1291. }
  1292. }
  1293. /**
  1294. * Restores the original saved mail system for migrations that require it.
  1295. */
  1296. public function restoreMailSystem() {
  1297. global $conf;
  1298. $conf['mail_system'] = $this->mailSystem;
  1299. }
  1300. }
  1301. // Make sure static members (in particular, $displayFunction) get
  1302. // initialized even if there are no class instances.
  1303. MigrationBase::staticInitialize();