PageRenderTime 42ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/wp-content/plugins/loco-translate/src/fs/FileWriter.php

https://gitlab.com/campus-academy/krowkaramel
PHP | 341 lines | 218 code | 32 blank | 91 comment | 27 complexity | 87dbd8362a00172884ecfee477088e8b MD5 | raw file
  1. <?php
  2. /**
  3. * Provides write operation context via the WordPress file system API
  4. */
  5. class Loco_fs_FileWriter {
  6. /**
  7. * @var Loco_fs_File
  8. */
  9. private $file;
  10. /**
  11. * @var WP_Filesystem_Base
  12. */
  13. private $fs;
  14. /**
  15. * @param Loco_fs_File
  16. */
  17. public function __construct( Loco_fs_File $file ){
  18. $this->file = $file;
  19. $this->disconnect();
  20. }
  21. /**
  22. * @param Loco_fs_File
  23. * @return Loco_fs_FileWriter
  24. */
  25. public function setFile( Loco_fs_File $file ){
  26. $this->file = $file;
  27. return $this;
  28. }
  29. /**
  30. * Connect to alternative file system context
  31. *
  32. * @param WP_Filesystem_Base
  33. * @param bool whether reconnect required
  34. * @return Loco_fs_FileWriter
  35. * @throws Loco_error_WriteException
  36. */
  37. public function connect( WP_Filesystem_Base $fs, $disconnected = true ){
  38. if( $disconnected && ! $fs->connect() ){
  39. $errors = $fs->errors;
  40. if( is_wp_error($errors) ){
  41. foreach( $errors->get_error_messages() as $reason ){
  42. Loco_error_AdminNotices::warn($reason);
  43. }
  44. }
  45. throw new Loco_error_WriteException( __('Failed to connect to remote server','loco-translate') );
  46. }
  47. $this->fs = $fs;
  48. return $this;
  49. }
  50. /**
  51. * Revert to direct file system connection
  52. * @return Loco_fs_FileWriter
  53. */
  54. public function disconnect(){
  55. $this->fs = Loco_api_WordPressFileSystem::direct();
  56. return $this;
  57. }
  58. /**
  59. * Get mapped path for use in indirect file system manipulation
  60. * @return string
  61. */
  62. public function getPath(){
  63. return $this->mapPath( $this->file->getPath() );
  64. }
  65. /**
  66. * Map virtual path for remote file system
  67. * @param string
  68. * @return string
  69. */
  70. private function mapPath( $path ){
  71. if( ! $this->isDirect() ){
  72. $base = untrailingslashit( Loco_fs_File::abs(loco_constant('WP_CONTENT_DIR')) );
  73. $snip = strlen($base);
  74. if( substr( $path, 0, $snip ) !== $base ){
  75. // fall back to default path in case of symlinks
  76. $base = trailingslashit(ABSPATH).'wp-content';
  77. $snip = strlen($base);
  78. if( substr( $path, 0, $snip ) !== $base ){
  79. throw new Loco_error_WriteException('Remote path must be under WP_CONTENT_DIR');
  80. }
  81. }
  82. $virt = $this->fs->wp_content_dir();
  83. if( false === $virt ){
  84. throw new Loco_error_WriteException('Failed to find WP_CONTENT_DIR via remote connection');
  85. }
  86. $virt = untrailingslashit( $virt );
  87. $path = substr_replace( $path, $virt, 0, $snip );
  88. }
  89. return $path;
  90. }
  91. /**
  92. * Test if a direct (not remote) file system
  93. * @return bool
  94. */
  95. public function isDirect(){
  96. return $this->fs instanceof WP_Filesystem_Direct;
  97. }
  98. /**
  99. * @return bool
  100. */
  101. public function writable(){
  102. return ! $this->disabled() && $this->fs->is_writable( $this->getPath() );
  103. }
  104. /**
  105. * @param int file mode integer e.g 0664
  106. * @param bool whether to set recursively (directories)
  107. * @return Loco_fs_FileWriter
  108. * @throws Loco_error_WriteException
  109. */
  110. public function chmod( $mode, $recursive = false ){
  111. $this->authorize();
  112. if( ! $this->fs->chmod( $this->getPath(), $mode, $recursive ) ){
  113. throw new Loco_error_WriteException( sprintf( __('Failed to chmod %s','loco-translate'), $this->file->basename() ) );
  114. }
  115. return $this;
  116. }
  117. /**
  118. * @param Loco_fs_File target for copy
  119. * @return Loco_fs_FileWriter
  120. * @throws Loco_error_WriteException
  121. */
  122. public function copy( Loco_fs_File $copy ){
  123. $this->authorize();
  124. $source = $this->getPath();
  125. $target = $this->mapPath( $copy->getPath() );
  126. // bugs in WP file system "exists" methods means we must force $overwrite=true; so checking file existence first
  127. if( $copy->exists() ){
  128. Loco_error_AdminNotices::debug(sprintf('Cannot copy %s to %s (target already exists)',$source,$target));
  129. throw new Loco_error_WriteException( __('Refusing to copy over an existing file','loco-translate') );
  130. }
  131. // ensure target directory exists, although in most cases copy will be in situ
  132. $parent = $copy->getParent();
  133. if( $parent && ! $parent->exists() ){
  134. $this->mkdir($parent);
  135. }
  136. // perform WP file system copy method
  137. if( ! $this->fs->copy($source,$target,true) ){
  138. Loco_error_AdminNotices::debug(sprintf('Failed to copy %s to %s via "%s" method',$source,$target,$this->fs->method));
  139. throw new Loco_error_WriteException( sprintf( __('Failed to copy %s to %s','loco-translate'), basename($source), basename($target) ) );
  140. }
  141. return $this;
  142. }
  143. /**
  144. * @param Loco_fs_File target file with new path
  145. * @return Loco_fs_FileWriter
  146. * @throws Loco_error_WriteException
  147. */
  148. public function move( Loco_fs_File $dest ){
  149. $orig = $this->file;
  150. try {
  151. // target should have been authorized to create the new file
  152. $context = clone $dest->getWriteContext();
  153. $context->setFile($orig);
  154. $context->copy($dest);
  155. // source should have been authorized to delete the original file
  156. $this->delete(false);
  157. return $this;
  158. }
  159. catch( Loco_error_WriteException $e ){
  160. Loco_error_AdminNotices::debug('copy/delete failure: '.$e->getMessage() );
  161. throw new Loco_error_WriteException( sprintf( 'Failed to move %s', $orig->basename() ) );
  162. }
  163. }
  164. /**
  165. * @param bool
  166. * @return Loco_fs_FileWriter
  167. * @throws Loco_error_WriteException
  168. */
  169. public function delete( $recursive = false ){
  170. $this->authorize();
  171. if( ! $this->fs->delete( $this->getPath(), $recursive ) ){
  172. throw new Loco_error_WriteException( sprintf( __('Failed to delete %s','loco-translate'), $this->file->basename() ) );
  173. }
  174. return $this;
  175. }
  176. /**
  177. * @param string
  178. * @return Loco_fs_FileWriter
  179. * @throws Loco_error_WriteException
  180. */
  181. public function putContents( $data ){
  182. $this->authorize();
  183. $file = $this->file;
  184. if( $file->isDirectory() ){
  185. throw new Loco_error_WriteException( sprintf( __('"%s" is a directory, not a file','loco-translate'), $file->basename() ) );
  186. }
  187. // file having no parent directory is likely an error, like a relative path.
  188. $dir = $file->getParent();
  189. if( ! $dir ){
  190. throw new Loco_error_WriteException( sprintf('Bad file path "%s"',$file) );
  191. }
  192. // avoid chmod of existing file
  193. if( $file->exists() ){
  194. $mode = $file->mode();
  195. }
  196. // may have bypassed definition of FS_CHMOD_FILE
  197. else {
  198. $mode = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644;
  199. // new file may also require directory path building
  200. if( ! $dir->exists() ){
  201. $this->mkdir($dir);
  202. }
  203. }
  204. $fs = $this->fs;
  205. $path = $this->getPath();
  206. if( ! $fs->put_contents($path,$data,$mode) ){
  207. // provide useful reason for failure if possible
  208. if( $file->exists() && ! $file->writable() ){
  209. Loco_error_AdminNotices::debug( sprintf('File not writable via "%s" method, check permissions on %s',$fs->method,$path) );
  210. throw new Loco_error_WriteException( __("Permission denied to update file",'loco-translate') );
  211. }
  212. // directory path should exist or have thrown error earlier.
  213. // directory path may not be writable by same fs context
  214. if( ! $dir->writable() ){
  215. Loco_error_AdminNotices::debug( sprintf('Directory not writable via "%s" method; check permissions for %s',$fs->method,$dir) );
  216. throw new Loco_error_WriteException( __("Parent directory isn't writable",'loco-translate') );
  217. }
  218. // else reason for failure is not established
  219. Loco_error_AdminNotices::debug( sprintf('Unknown write failure via "%s" method; check %s',$fs->method,$path) );
  220. throw new Loco_error_WriteException( __('Failed to save file','loco-translate').': '.$file->basename() );
  221. }
  222. // trigger hook every time a file is written. This allows caches to be invalidated
  223. try {
  224. do_action( 'loco_file_written', $path );
  225. }
  226. catch( Exception $e ){
  227. Loco_error_AdminNotices::add( Loco_error_Exception::convert($e) );
  228. }
  229. return $this;
  230. }
  231. /**
  232. * Create current directory context
  233. * @param Loco_fs_File optional directory
  234. * @return bool
  235. * @throws Loco_error_WriteException
  236. */
  237. public function mkdir( Loco_fs_File $here = null ) {
  238. if( is_null($here) ){
  239. $here = $this->file;
  240. }
  241. $this->authorize();
  242. $fs = $this->fs;
  243. // may have bypassed definition of FS_CHMOD_DIR
  244. $mode = defined('FS_CHMOD_DIR') ? FS_CHMOD_DIR : 0755;
  245. // find first ancestor that exists while building tree
  246. $stack = [];
  247. /* @var $parent Loco_fs_Directory */
  248. while( $parent = $here->getParent() ){
  249. array_unshift( $stack, $this->mapPath( $here->getPath() ) );
  250. if( $parent->exists() ){
  251. // have existent directory, now build full path
  252. foreach( $stack as $path ){
  253. if( ! $fs->mkdir($path,$mode) ){
  254. Loco_error_AdminNotices::debug( sprintf('mkdir(%s,%03o) failed via "%s" method;',var_export($path,1),$mode,$fs->method) );
  255. throw new Loco_error_WriteException( __('Failed to create directory','loco-translate') );
  256. }
  257. }
  258. return true;
  259. }
  260. $here = $parent;
  261. }
  262. // refusing to create directory when the entire path is missing. e.g. "/bad"
  263. throw new Loco_error_WriteException( __('Failed to build directory path','loco-translate') );
  264. }
  265. /**
  266. * Check whether write operations are permitted, or throw
  267. * @throws Loco_error_WriteException
  268. * @return Loco_fs_FileWriter
  269. */
  270. public function authorize(){
  271. if( $this->disabled() ){
  272. throw new Loco_error_WriteException( __('File modification is disallowed by your WordPress config','loco-translate') );
  273. }
  274. $opts = Loco_data_Settings::get();
  275. // deny system file changes (fs_protect = 2)
  276. if( 1 < $opts->fs_protect && $this->file->getUpdateType() ){
  277. throw new Loco_error_WriteException( __('Modification of installed files is disallowed by the plugin settings','loco-translate') );
  278. }
  279. // deny POT modification (pot_protect = 2)
  280. // this assumes that templates all have .pot extension, which isn't guaranteed. UI should prevent saving of wrongly files like "default.po"
  281. if( 'pot' === strtolower($this->file->extension()) && 1 < $opts->pot_protect ){
  282. throw new Loco_error_WriteException( __('Modification of POT (template) files is disallowed by the plugin settings','loco-translate') );
  283. }
  284. // Deny list of executable file extensions, noting that specific actions may limit this further.
  285. // Note that this ignores the base file name, so "php.pot" would be permitted, but "foo.php.pot" would not.
  286. $exts = array_slice( explode('.', $this->file->basename() ), 1 );
  287. if( preg_grep('/^php\\d*/i', $exts ) ){
  288. throw new Loco_error_WriteException('Executable file extension disallowed .'.implode('.',$exts) );
  289. }
  290. return $this;
  291. }
  292. /**
  293. * Check if file system modification is banned at WordPress level
  294. * @return bool
  295. */
  296. public function disabled(){
  297. // WordPress >= 4.8
  298. if( function_exists('wp_is_file_mod_allowed') ){
  299. $context = apply_filters( 'loco_file_mod_allowed_context', 'download_language_pack', $this->file );
  300. return ! wp_is_file_mod_allowed( $context );
  301. }
  302. // fall back to direct constant check
  303. return (bool) loco_constant('DISALLOW_FILE_MODS');
  304. }
  305. }