1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16:
17:
18: App::uses('AppShell', 'Console/Command');
19: App::uses('File', 'Utility');
20: App::uses('Folder', 'Utility');
21: App::uses('Hash', 'Utility');
22:
23: 24: 25: 26: 27:
28: class ExtractTask extends AppShell {
29:
30: 31: 32: 33: 34:
35: protected $_paths = array();
36:
37: 38: 39: 40: 41:
42: protected $_files = array();
43:
44: 45: 46: 47: 48:
49: protected $_merge = false;
50:
51: 52: 53: 54: 55:
56: protected $_file = null;
57:
58: 59: 60: 61: 62:
63: protected $_storage = array();
64:
65: 66: 67: 68: 69:
70: protected $_tokens = array();
71:
72: 73: 74: 75: 76:
77: protected $_translations = array();
78:
79: 80: 81: 82: 83:
84: protected $_output = null;
85:
86: 87: 88: 89: 90:
91: protected $_exclude = array();
92:
93: 94: 95: 96: 97:
98: protected $_extractValidation = true;
99:
100: 101: 102: 103: 104:
105: protected $_validationDomain = 'default';
106:
107: 108: 109: 110: 111:
112: protected $_extractCore = false;
113:
114: 115: 116: 117: 118:
119: protected function _getPaths() {
120: $defaultPath = APP;
121: while (true) {
122: $currentPaths = count($this->_paths) > 0 ? $this->_paths : array('None');
123: $message = __d(
124: 'cake_console',
125: "Current paths: %s\nWhat is the path you would like to extract?\n[Q]uit [D]one",
126: implode(', ', $currentPaths)
127: );
128: $response = $this->in($message, null, $defaultPath);
129: if (strtoupper($response) === 'Q') {
130: $this->err(__d('cake_console', 'Extract Aborted'));
131: return $this->_stop();
132: } elseif (strtoupper($response) === 'D' && count($this->_paths)) {
133: $this->out();
134: return;
135: } elseif (strtoupper($response) === 'D') {
136: $this->err(__d('cake_console', '<warning>No directories selected.</warning> Please choose a directory.'));
137: } elseif (is_dir($response)) {
138: $this->_paths[] = $response;
139: $defaultPath = 'D';
140: } else {
141: $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.'));
142: }
143: $this->out();
144: }
145: }
146:
147: 148: 149: 150: 151:
152: public function execute() {
153: if (!empty($this->params['exclude'])) {
154: $this->_exclude = explode(',', str_replace('/', DS, $this->params['exclude']));
155: }
156: if (isset($this->params['files']) && !is_array($this->params['files'])) {
157: $this->_files = explode(',', $this->params['files']);
158: }
159: if (isset($this->params['paths'])) {
160: $this->_paths = explode(',', $this->params['paths']);
161: } elseif (isset($this->params['plugin'])) {
162: $plugin = Inflector::camelize($this->params['plugin']);
163: if (!CakePlugin::loaded($plugin)) {
164: CakePlugin::load($plugin);
165: }
166: $this->_paths = array(CakePlugin::path($plugin));
167: $this->params['plugin'] = $plugin;
168: } else {
169: $this->_getPaths();
170: }
171:
172: if (isset($this->params['extract-core'])) {
173: $this->_extractCore = !(strtolower($this->params['extract-core']) === 'no');
174: } else {
175: $response = $this->in(__d('cake_console', 'Would you like to extract the messages from the CakePHP core?'), array('y', 'n'), 'n');
176: $this->_extractCore = strtolower($response) === 'y';
177: }
178:
179: if (!empty($this->params['exclude-plugins']) && $this->_isExtractingApp()) {
180: $this->_exclude = array_merge($this->_exclude, App::path('plugins'));
181: }
182:
183: if (!empty($this->params['ignore-model-validation']) || (!$this->_isExtractingApp() && empty($plugin))) {
184: $this->_extractValidation = false;
185: }
186: if (!empty($this->params['validation-domain'])) {
187: $this->_validationDomain = $this->params['validation-domain'];
188: }
189:
190: if ($this->_extractCore) {
191: $this->_paths[] = CAKE;
192: $this->_exclude = array_merge($this->_exclude, array(
193: CAKE . 'Test',
194: CAKE . 'Console' . DS . 'Templates'
195: ));
196: }
197:
198: if (isset($this->params['output'])) {
199: $this->_output = $this->params['output'];
200: } elseif (isset($this->params['plugin'])) {
201: $this->_output = $this->_paths[0] . DS . 'Locale';
202: } else {
203: $message = __d('cake_console', "What is the path you would like to output?\n[Q]uit", $this->_paths[0] . DS . 'Locale');
204: while (true) {
205: $response = $this->in($message, null, rtrim($this->_paths[0], DS) . DS . 'Locale');
206: if (strtoupper($response) === 'Q') {
207: $this->err(__d('cake_console', 'Extract Aborted'));
208: return $this->_stop();
209: } elseif ($this->_isPathUsable($response)) {
210: $this->_output = $response . DS;
211: break;
212: } else {
213: $this->err(__d('cake_console', 'The directory path you supplied was not found. Please try again.'));
214: }
215: $this->out();
216: }
217: }
218:
219: if (isset($this->params['merge'])) {
220: $this->_merge = !(strtolower($this->params['merge']) === 'no');
221: } else {
222: $this->out();
223: $response = $this->in(__d('cake_console', 'Would you like to merge all domain and category strings into the default.pot file?'), array('y', 'n'), 'n');
224: $this->_merge = strtolower($response) === 'y';
225: }
226:
227: if (empty($this->_files)) {
228: $this->_searchFiles();
229: }
230:
231: $this->_output = rtrim($this->_output, DS) . DS;
232: if (!$this->_isPathUsable($this->_output)) {
233: $this->err(__d('cake_console', 'The output directory %s was not found or writable.', $this->_output));
234: return $this->_stop();
235: }
236:
237: $this->_extract();
238: }
239:
240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250:
251: protected function _addTranslation($category, $domain, $msgid, $details = array()) {
252: $context = '';
253: if (isset($details['msgctxt'])) {
254: $context = $details['msgctxt'];
255: }
256:
257: if (empty($this->_translations[$category][$domain][$msgid][$context])) {
258: $this->_translations[$category][$domain][$msgid][$context] = array(
259: 'msgid_plural' => false,
260: );
261: }
262:
263: if (isset($details['msgid_plural'])) {
264: $this->_translations[$category][$domain][$msgid][$context]['msgid_plural'] = $details['msgid_plural'];
265: }
266: if (isset($details['file'])) {
267: $line = 0;
268: if (isset($details['line'])) {
269: $line = $details['line'];
270: }
271: $this->_translations[$category][$domain][$msgid][$context]['references'][$details['file']][] = $line;
272: }
273: }
274:
275: 276: 277: 278: 279:
280: protected function _extract() {
281: $this->out();
282: $this->out();
283: $this->out(__d('cake_console', 'Extracting...'));
284: $this->hr();
285: $this->out(__d('cake_console', 'Paths:'));
286: foreach ($this->_paths as $path) {
287: $this->out(' ' . $path);
288: }
289: $this->out(__d('cake_console', 'Output Directory: ') . $this->_output);
290: $this->hr();
291: $this->_extractTokens();
292: $this->_extractValidationMessages();
293: $this->_buildFiles();
294: $this->_writeFiles();
295: $this->_paths = $this->_files = $this->_storage = array();
296: $this->_translations = $this->_tokens = array();
297: $this->_extractValidation = true;
298: $this->out();
299: $this->out(__d('cake_console', 'Done.'));
300: }
301:
302: 303: 304: 305: 306:
307: public function getOptionParser() {
308: $parser = parent::getOptionParser();
309:
310: $parser->description(
311: __d('cake_console', 'CakePHP Language String Extraction:')
312: )->addOption('app', array(
313: 'help' => __d('cake_console', 'Directory where your application is located.')
314: ))->addOption('paths', array(
315: 'help' => __d('cake_console', 'Comma separated list of paths.')
316: ))->addOption('merge', array(
317: 'help' => __d('cake_console', 'Merge all domain and category strings into the default.po file.'),
318: 'choices' => array('yes', 'no')
319: ))->addOption('no-location', array(
320: 'boolean' => true,
321: 'default' => false,
322: 'help' => __d('cake_console', 'Do not write lines with locations'),
323: ))->addOption('output', array(
324: 'help' => __d('cake_console', 'Full path to output directory.')
325: ))->addOption('files', array(
326: 'help' => __d('cake_console', 'Comma separated list of files.')
327: ))->addOption('exclude-plugins', array(
328: 'boolean' => true,
329: 'default' => true,
330: 'help' => __d('cake_console', 'Ignores all files in plugins if this command is run inside from the same app directory.')
331: ))->addOption('plugin', array(
332: 'help' => __d('cake_console', 'Extracts tokens only from the plugin specified and puts the result in the plugin\'s Locale directory.')
333: ))->addOption('ignore-model-validation', array(
334: 'boolean' => true,
335: 'default' => false,
336: 'help' => __d('cake_console', 'Ignores validation messages in the $validate property.' .
337: ' If this flag is not set and the command is run from the same app directory,' .
338: ' all messages in model validation rules will be extracted as tokens.'
339: )
340: ))->addOption('validation-domain', array(
341: 'help' => __d('cake_console', 'If set to a value, the localization domain to be used for model validation messages.')
342: ))->addOption('exclude', array(
343: 'help' => __d('cake_console', 'Comma separated list of directories to exclude.' .
344: ' Any path containing a path segment with the provided values will be skipped. E.g. test,vendors'
345: )
346: ))->addOption('overwrite', array(
347: 'boolean' => true,
348: 'default' => false,
349: 'help' => __d('cake_console', 'Always overwrite existing .pot files.')
350: ))->addOption('extract-core', array(
351: 'help' => __d('cake_console', 'Extract messages from the CakePHP core libs.'),
352: 'choices' => array('yes', 'no')
353: ));
354:
355: return $parser;
356: }
357:
358: 359: 360: 361: 362:
363: protected function _extractTokens() {
364: foreach ($this->_files as $file) {
365: $this->_file = $file;
366: $this->out(__d('cake_console', 'Processing %s...', $file), 1, Shell::VERBOSE);
367:
368: $code = file_get_contents($file);
369: $allTokens = token_get_all($code);
370:
371: $this->_tokens = array();
372: foreach ($allTokens as $token) {
373: if (!is_array($token) || ($token[0] !== T_WHITESPACE && $token[0] !== T_INLINE_HTML)) {
374: $this->_tokens[] = $token;
375: }
376: }
377: unset($allTokens);
378: $this->_parse('__', array('singular'));
379: $this->_parse('__n', array('singular', 'plural'));
380: $this->_parse('__d', array('domain', 'singular'));
381: $this->_parse('__c', array('singular', 'category'));
382: $this->_parse('__dc', array('domain', 'singular', 'category'));
383: $this->_parse('__dn', array('domain', 'singular', 'plural'));
384: $this->_parse('__dcn', array('domain', 'singular', 'plural', 'count', 'category'));
385:
386: $this->_parse('__x', array('context', 'singular'));
387: $this->_parse('__xn', array('context', 'singular', 'plural'));
388: $this->_parse('__dx', array('domain', 'context', 'singular'));
389: $this->_parse('__dxc', array('domain', 'context', 'singular', 'category'));
390: $this->_parse('__dxn', array('domain', 'context', 'singular', 'plural'));
391: $this->_parse('__dxcn', array('domain', 'context', 'singular', 'plural', 'count', 'category'));
392: $this->_parse('__xc', array('context', 'singular', 'category'));
393:
394: }
395: }
396:
397: 398: 399: 400: 401: 402: 403:
404: protected function _parse($functionName, $map) {
405: $count = 0;
406: $categories = array('LC_ALL', 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME', 'LC_MESSAGES');
407: $tokenCount = count($this->_tokens);
408:
409: while (($tokenCount - $count) > 1) {
410: $countToken = $this->_tokens[$count];
411: $firstParenthesis = $this->_tokens[$count + 1];
412: if (!is_array($countToken)) {
413: $count++;
414: continue;
415: }
416:
417: list($type, $string, $line) = $countToken;
418: if (($type == T_STRING) && ($string === $functionName) && ($firstParenthesis === '(')) {
419: $position = $count;
420: $depth = 0;
421:
422: while (!$depth) {
423: if ($this->_tokens[$position] === '(') {
424: $depth++;
425: } elseif ($this->_tokens[$position] === ')') {
426: $depth--;
427: }
428: $position++;
429: }
430:
431: $mapCount = count($map);
432: $strings = $this->_getStrings($position, $mapCount);
433:
434: if ($mapCount === count($strings)) {
435: extract(array_combine($map, $strings));
436: $category = isset($category) ? $category : 6;
437: $category = (int)$category;
438: $categoryName = $categories[$category];
439:
440: $domain = isset($domain) ? $domain : 'default';
441: $details = array(
442: 'file' => $this->_file,
443: 'line' => $line,
444: );
445: if (isset($plural)) {
446: $details['msgid_plural'] = $plural;
447: }
448: if (isset($context)) {
449: $details['msgctxt'] = $context;
450: }
451:
452: if ($categoryName !== 'LC_TIME') {
453: $this->_addTranslation($categoryName, $domain, $singular, $details);
454: }
455: } elseif (!is_array($this->_tokens[$count - 1]) || $this->_tokens[$count - 1][0] != T_FUNCTION) {
456: $this->_markerError($this->_file, $line, $functionName, $count);
457: }
458: }
459: $count++;
460: }
461: }
462:
463: 464: 465: 466: 467: 468:
469: protected function _extractValidationMessages() {
470: if (!$this->_extractValidation) {
471: return;
472: }
473:
474: $plugins = array(null);
475: if (empty($this->params['exclude-plugins'])) {
476: $plugins = array_merge($plugins, App::objects('plugin', null, false));
477: }
478: foreach ($plugins as $plugin) {
479: $this->_extractPluginValidationMessages($plugin);
480: }
481: }
482:
483: 484: 485: 486: 487: 488:
489: protected function _extractPluginValidationMessages($plugin = null) {
490: App::uses('AppModel', 'Model');
491: if (!empty($plugin)) {
492: if (!CakePlugin::loaded($plugin)) {
493: return;
494: }
495: App::uses($plugin . 'AppModel', $plugin . '.Model');
496: $plugin = $plugin . '.';
497: }
498: $models = App::objects($plugin . 'Model', null, false);
499:
500: foreach ($models as $model) {
501: App::uses($model, $plugin . 'Model');
502: $reflection = new ReflectionClass($model);
503: if (!$reflection->isSubClassOf('Model')) {
504: continue;
505: }
506: $properties = $reflection->getDefaultProperties();
507: $validate = $properties['validate'];
508: if (empty($validate)) {
509: continue;
510: }
511:
512: $file = $reflection->getFileName();
513: $domain = $this->_validationDomain;
514: if (!empty($properties['validationDomain'])) {
515: $domain = $properties['validationDomain'];
516: }
517: foreach ($validate as $field => $rules) {
518: $this->_processValidationRules($field, $rules, $file, $domain);
519: }
520: }
521: }
522:
523: 524: 525: 526: 527: 528: 529: 530: 531: 532: 533:
534: protected function _processValidationRules($field, $rules, $file, $domain, $category = 'LC_MESSAGES') {
535: if (!is_array($rules)) {
536: return;
537: }
538:
539: $dims = Hash::dimensions($rules);
540: if ($dims === 1 || ($dims === 2 && isset($rules['message']))) {
541: $rules = array($rules);
542: }
543:
544: foreach ($rules as $rule => $validateProp) {
545: $msgid = null;
546: if (isset($validateProp['message'])) {
547: if (is_array($validateProp['message'])) {
548: $msgid = $validateProp['message'][0];
549: } else {
550: $msgid = $validateProp['message'];
551: }
552: } elseif (is_string($rule)) {
553: $msgid = $rule;
554: }
555: if ($msgid) {
556: $msgid = $this->_formatString(sprintf("'%s'", $msgid));
557: $details = array(
558: 'file' => $file,
559: 'line' => 'validation for field ' . $field
560: );
561: $this->_addTranslation($category, $domain, $msgid, $details);
562: }
563: }
564: }
565:
566: 567: 568: 569: 570:
571: protected function _buildFiles() {
572: $paths = $this->_paths;
573: $paths[] = realpath(APP) . DS;
574:
575: usort($paths, function ($a, $b) {
576: return strlen($b) - strlen($a);
577: });
578:
579: foreach ($this->_translations as $category => $domains) {
580: foreach ($domains as $domain => $translations) {
581: foreach ($translations as $msgid => $contexts) {
582: foreach ($contexts as $context => $details) {
583: $plural = $details['msgid_plural'];
584: $header = '';
585: if (empty($this->params['no-location'])) {
586: $files = $details['references'];
587: $occurrences = array();
588: foreach ($files as $file => $lines) {
589: $lines = array_unique($lines);
590: $occurrences[] = $file . ':' . implode(';', $lines);
591: }
592: $occurrences = implode("\n#: ", $occurrences);
593: $header = '#: ' . str_replace(DS, '/', str_replace($paths, '', $occurrences)) . "\n";
594: }
595:
596: $sentence = '';
597: if ($context) {
598: $sentence .= "msgctxt \"{$context}\"\n";
599: }
600: if ($plural === false) {
601: $sentence .= "msgid \"{$msgid}\"\n";
602: $sentence .= "msgstr \"\"\n\n";
603: } else {
604: $sentence .= "msgid \"{$msgid}\"\n";
605: $sentence .= "msgid_plural \"{$plural}\"\n";
606: $sentence .= "msgstr[0] \"\"\n";
607: $sentence .= "msgstr[1] \"\"\n\n";
608: }
609:
610: $this->_store($category, $domain, $header, $sentence);
611: if (($category !== 'LC_MESSAGES' || $domain !== 'default') && $this->_merge) {
612: $this->_store('LC_MESSAGES', 'default', $header, $sentence);
613: }
614: }
615: }
616: }
617: }
618: }
619:
620: 621: 622: 623: 624: 625: 626: 627: 628:
629: protected function _store($category, $domain, $header, $sentence) {
630: if (!isset($this->_storage[$category])) {
631: $this->_storage[$category] = array();
632: }
633: if (!isset($this->_storage[$category][$domain])) {
634: $this->_storage[$category][$domain] = array();
635: }
636: if (!isset($this->_storage[$category][$domain][$sentence])) {
637: $this->_storage[$category][$domain][$sentence] = $header;
638: } else {
639: $this->_storage[$category][$domain][$sentence] .= $header;
640: }
641: }
642:
643: 644: 645: 646: 647:
648: protected function _writeFiles() {
649: $overwriteAll = false;
650: if (!empty($this->params['overwrite'])) {
651: $overwriteAll = true;
652: }
653: foreach ($this->_storage as $category => $domains) {
654: foreach ($domains as $domain => $sentences) {
655: $output = $this->_writeHeader();
656: foreach ($sentences as $sentence => $header) {
657: $output .= $header . $sentence;
658: }
659:
660: $filename = $domain . '.pot';
661: if ($category === 'LC_MESSAGES') {
662: $File = new File($this->_output . $filename);
663: } else {
664: new Folder($this->_output . $category, true);
665: $File = new File($this->_output . $category . DS . $filename);
666: }
667: $response = '';
668: while ($overwriteAll === false && $File->exists() && strtoupper($response) !== 'Y') {
669: $this->out();
670: $response = $this->in(
671: __d('cake_console', 'Error: %s already exists in this location. Overwrite? [Y]es, [N]o, [A]ll', $filename),
672: array('y', 'n', 'a'),
673: 'y'
674: );
675: if (strtoupper($response) === 'N') {
676: $response = '';
677: while (!$response) {
678: $response = $this->in(__d('cake_console', "What would you like to name this file?"), null, 'new_' . $filename);
679: $File = new File($this->_output . $response);
680: $filename = $response;
681: }
682: } elseif (strtoupper($response) === 'A') {
683: $overwriteAll = true;
684: }
685: }
686: $File->write($output);
687: $File->close();
688: }
689: }
690: }
691:
692: 693: 694: 695: 696:
697: protected function _writeHeader() {
698: $output = "# LANGUAGE translation of CakePHP Application\n";
699: $output .= "# Copyright YEAR NAME <EMAIL@ADDRESS>\n";
700: $output .= "#\n";
701: $output .= "#, fuzzy\n";
702: $output .= "msgid \"\"\n";
703: $output .= "msgstr \"\"\n";
704: $output .= "\"Project-Id-Version: PROJECT VERSION\\n\"\n";
705: $output .= "\"PO-Revision-Date: YYYY-mm-DD HH:MM+ZZZZ\\n\"\n";
706: $output .= "\"Last-Translator: NAME <EMAIL@ADDRESS>\\n\"\n";
707: $output .= "\"Language-Team: LANGUAGE <EMAIL@ADDRESS>\\n\"\n";
708: $output .= "\"MIME-Version: 1.0\\n\"\n";
709: $output .= "\"Content-Type: text/plain; charset=utf-8\\n\"\n";
710: $output .= "\"Content-Transfer-Encoding: 8bit\\n\"\n";
711: $output .= "\"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n\"\n\n";
712: return $output;
713: }
714:
715: 716: 717: 718: 719: 720: 721:
722: protected function _getStrings(&$position, $target) {
723: $strings = array();
724: $count = count($strings);
725: while ($count < $target && ($this->_tokens[$position] === ',' || $this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position][0] == T_LNUMBER)) {
726: $count = count($strings);
727: if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING && $this->_tokens[$position + 1] === '.') {
728: $string = '';
729: while ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING || $this->_tokens[$position] === '.') {
730: if ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
731: $string .= $this->_formatString($this->_tokens[$position][1]);
732: }
733: $position++;
734: }
735: $strings[] = $string;
736: } elseif ($this->_tokens[$position][0] == T_CONSTANT_ENCAPSED_STRING) {
737: $strings[] = $this->_formatString($this->_tokens[$position][1]);
738: } elseif ($this->_tokens[$position][0] == T_LNUMBER) {
739: $strings[] = $this->_tokens[$position][1];
740: }
741: $position++;
742: }
743: return $strings;
744: }
745:
746: 747: 748: 749: 750: 751:
752: protected function _formatString($string) {
753: $quote = substr($string, 0, 1);
754: $string = substr($string, 1, -1);
755: if ($quote === '"') {
756: $string = stripcslashes($string);
757: } else {
758: $string = strtr($string, array("\\'" => "'", "\\\\" => "\\"));
759: }
760: $string = str_replace("\r\n", "\n", $string);
761: return addcslashes($string, "\0..\37\\\"");
762: }
763:
764: 765: 766: 767: 768: 769: 770: 771: 772:
773: protected function _markerError($file, $line, $marker, $count) {
774: $this->err(__d('cake_console', "Invalid marker content in %s:%s\n* %s(", $file, $line, $marker));
775: $count += 2;
776: $tokenCount = count($this->_tokens);
777: $parenthesis = 1;
778:
779: while ((($tokenCount - $count) > 0) && $parenthesis) {
780: if (is_array($this->_tokens[$count])) {
781: $this->err($this->_tokens[$count][1], false);
782: } else {
783: $this->err($this->_tokens[$count], false);
784: if ($this->_tokens[$count] === '(') {
785: $parenthesis++;
786: }
787:
788: if ($this->_tokens[$count] === ')') {
789: $parenthesis--;
790: }
791: }
792: $count++;
793: }
794: $this->err("\n", true);
795: }
796:
797: 798: 799: 800: 801:
802: protected function _searchFiles() {
803: $pattern = false;
804: if (!empty($this->_exclude)) {
805: $exclude = array();
806: foreach ($this->_exclude as $e) {
807: if (DS !== '\\' && $e[0] !== DS) {
808: $e = DS . $e;
809: }
810: $exclude[] = preg_quote($e, '/');
811: }
812: $pattern = '/' . implode('|', $exclude) . '/';
813: }
814: foreach ($this->_paths as $i => $path) {
815: $this->_paths[$i] = realpath($path) . DS;
816: $Folder = new Folder($this->_paths[$i]);
817: $files = $Folder->findRecursive('.*\.(php|ctp|thtml|inc|tpl)', true);
818: if (!empty($pattern)) {
819: $files = preg_grep($pattern, $files, PREG_GREP_INVERT);
820: $files = array_values($files);
821: }
822: $this->_files = array_merge($this->_files, $files);
823: }
824: $this->_files = array_unique($this->_files);
825: }
826:
827: 828: 829: 830: 831: 832:
833: protected function _isExtractingApp() {
834: return $this->_paths === array(APP);
835: }
836:
837: 838: 839: 840: 841: 842:
843: protected function _isPathUsable($path) {
844: return is_dir($path) && is_writable($path);
845: }
846: }
847: