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

/system/expressionengine/modules/rte/libraries/Rte_lib.php

https://bitbucket.org/tdevonshire/hoolux
PHP | 698 lines | 429 code | 112 blank | 157 comment | 66 complexity | e962ce17de91de4d3eca4a7f2f4f5c41 MD5 | raw file
  1. <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');
  2. /**
  3. * ExpressionEngine - by EllisLab
  4. *
  5. * @package ExpressionEngine
  6. * @author EllisLab Dev Team
  7. * @copyright Copyright (c) 2003 - 2012, EllisLab, Inc.
  8. * @license http://ellislab.com/expressionengine/user-guide/license.html
  9. * @link http://ellislab.com
  10. * @since Version 2.0
  11. * @filesource
  12. */
  13. // ------------------------------------------------------------------------
  14. /**
  15. * ExpressionEngine RTE Module Library
  16. *
  17. * @package ExpressionEngine
  18. * @subpackage Libraries
  19. * @category Modules
  20. * @author EllisLab Dev Team
  21. * @link http://ellislab.com
  22. */
  23. class Rte_lib {
  24. // We consider the editor empty in these cases
  25. public $_empty = array(
  26. '',
  27. '<br>',
  28. '<br/>',
  29. '<br />',
  30. '<p></p>',
  31. '<p>​</p>' // Zero-width character
  32. );
  33. public function __construct()
  34. {
  35. $this->EE =& get_instance();
  36. $this->EE->lang->loadfile('rte');
  37. }
  38. // -------------------------------------------------------------------------
  39. /**
  40. * Provides Edit Toolset Screen HTML
  41. *
  42. * @access public
  43. * @param int $toolset_id The Toolset ID to be edited (optional)
  44. * @return string The page
  45. */
  46. public function edit_toolset($toolset_id = FALSE)
  47. {
  48. if ($toolset_id === FALSE)
  49. {
  50. $toolset_id = $this->EE->input->get_post('toolset_id');
  51. }
  52. if ( ! is_numeric($toolset_id))
  53. {
  54. exit();
  55. }
  56. $this->EE->output->enable_profiler(FALSE);
  57. $this->EE->load->library(array('table','javascript'));
  58. $this->EE->load->model(array('rte_toolset_model','rte_tool_model'));
  59. // new toolset?
  60. if ($toolset_id == 0)
  61. {
  62. $toolset['tools'] = array();
  63. $toolset['name'] = '';
  64. $is_private = ($this->EE->input->get_post('private') == 'true');
  65. }
  66. else
  67. {
  68. // make sure user can access the existing toolset
  69. if ( ! $this->EE->rte_toolset_model->member_can_access($toolset_id))
  70. {
  71. $this->EE->output->send_ajax_response(array(
  72. 'error' => lang('toolset_edit_failed')
  73. ));
  74. }
  75. // grab the toolset
  76. $toolset = $this->EE->rte_toolset_model->get($toolset_id);
  77. $is_private = ($toolset['member_id'] != 0);
  78. }
  79. // get list of enabled tools
  80. $enabled_tools = $this->EE->rte_tool_model->get_tool_list(TRUE);
  81. $unused_tools = $used_tools = array();
  82. foreach ($enabled_tools as $tool)
  83. {
  84. $tool_index = array_search($tool['tool_id'], $toolset['tools']);
  85. // is the tool in this toolset?
  86. if ($tool_index !== FALSE)
  87. {
  88. $used_tools[$tool_index] = $tool;
  89. }
  90. else
  91. {
  92. $unused_tools[] = $tool;
  93. }
  94. }
  95. // sort used tools by custom order
  96. ksort($used_tools, SORT_NUMERIC);
  97. // set up the form
  98. $vars = array(
  99. 'action' => $this->form_url.AMP.'method=save_toolset'.( !! $toolset_id ? AMP.'toolset_id='.$toolset_id : ''),
  100. 'is_private' => $is_private,
  101. 'toolset_name' => ( ! $toolset || $is_private ? '' : $toolset['name']),
  102. 'available_tools' => $enabled_tools,
  103. 'unused_tools' => $unused_tools,
  104. 'used_tools' => $used_tools
  105. );
  106. // JS
  107. $this->EE->cp->add_js_script(array(
  108. 'ui' => 'sortable',
  109. 'file' => 'cp/rte'
  110. ));
  111. // CSS
  112. $this->EE->cp->add_to_head($this->EE->view->head_link('css/rte.css'));
  113. // return the form
  114. $this->EE->output->send_ajax_response(array(
  115. 'success' => $this->EE->load->view('edit_toolset', $vars, TRUE)
  116. ));
  117. }
  118. // --------------------------------------------------------------------
  119. /**
  120. * Saves a toolset
  121. *
  122. * @access public
  123. * @return void
  124. */
  125. public function save_toolset()
  126. {
  127. $this->EE->output->enable_profiler(FALSE);
  128. $this->EE->load->model('rte_toolset_model');
  129. // get the toolset
  130. $toolset_id = $this->EE->input->get_post('toolset_id');
  131. $toolset = array(
  132. 'name' => $this->EE->input->get_post('toolset_name'),
  133. 'tools' => $this->EE->input->get_post('selected_tools'),
  134. 'member_id' => ($this->EE->input->get_post('private') == 'true' ? $this->EE->session->userdata('member_id') : 0)
  135. );
  136. // is this an individual’s private toolset?
  137. $is_members = ($this->EE->input->get_post('private') == 'true');
  138. // did an empty name sneak through?
  139. if (empty($toolset['name']))
  140. {
  141. $this->EE->output->send_ajax_response(array(
  142. 'error' => lang('name_required')
  143. ));
  144. }
  145. // is the name unique?
  146. if ( ! $is_members && ! $this->EE->rte_toolset_model->unique_name($toolset['name'], $toolset_id))
  147. {
  148. $this->EE->output->send_ajax_response(array(
  149. 'error' => lang('unique_name_required')
  150. ));
  151. }
  152. // Updating? Make sure the toolset exists and they aren't trying any
  153. // funny business...
  154. if ($toolset_id)
  155. {
  156. $orig = $this->EE->rte_toolset_model->get($toolset_id);
  157. if ( ! $orig || $is_members && $orig['member_id'] != $this->EE->session->userdata('member_id'))
  158. {
  159. $this->EE->output->send_ajax_response(array(
  160. 'error' => lang('toolset_update_failed')
  161. ));
  162. }
  163. }
  164. // save it
  165. if ($this->EE->rte_toolset_model->save_toolset($toolset, $toolset_id) === FALSE)
  166. {
  167. $this->EE->output->send_ajax_response(array(
  168. 'error' => lang('toolset_update_failed')
  169. ));
  170. }
  171. // if it’s new, get the ID
  172. if ( ! $toolset_id)
  173. {
  174. $toolset_id = $this->EE->db->insert_id();
  175. }
  176. // If the default toolset was deleted
  177. if ($this->EE->config->item('rte_default_toolset_id') == 0)
  178. {
  179. $this->EE->config->update_site_prefs(array(
  180. 'rte_default_toolset_id' => $toolset_id
  181. ));
  182. }
  183. // update the member profile
  184. if ($is_members && $toolset_id)
  185. {
  186. $this->EE->db
  187. ->where('member_id', $this->EE->session->userdata('member_id'))
  188. ->update('members', array('rte_toolset_id' => $toolset_id));
  189. }
  190. $this->EE->output->send_ajax_response(array(
  191. 'success' => lang('toolset_updated'),
  192. 'force_refresh' => TRUE
  193. ));
  194. }
  195. // ------------------------------------------------------------------------
  196. /**
  197. * Build RTE JS
  198. *
  199. * @access private
  200. * @param int The ID of the toolset you want to load
  201. * @param string The selector that will match the elements to turn into an RTE
  202. * @param array Include or skip certain JS libs: array('jquery' => FALSE //skip)
  203. * @param bool If TRUE, includes tools that are for the control panel only
  204. * @return string The JS needed to embed the RTE
  205. */
  206. public function build_js($toolset_id, $selector, $include = array(), $cp_only = FALSE)
  207. {
  208. $this->EE->load->model(array('rte_toolset_model','rte_tool_model'));
  209. // no toolset specified?
  210. if ( ! $toolset_id)
  211. {
  212. $toolset_id = $this->EE->rte_toolset_model->get_member_toolset();
  213. }
  214. // get the toolset
  215. $toolset = $this->EE->rte_toolset_model->get($toolset_id);
  216. if ( ! $toolset OR ! $toolset['tools'])
  217. {
  218. return;
  219. }
  220. // get the tools
  221. if ( ! $tools = $this->EE->rte_tool_model->get_tools($toolset['tools']))
  222. {
  223. return;
  224. }
  225. // bare minimum required
  226. $bits = array(
  227. 'globals' => array(
  228. 'rte' => array()
  229. ),
  230. 'styles' => '',
  231. 'definitions' => '',
  232. 'buttons' => array(),
  233. 'libraries' => array(
  234. 'plugin' => array('wysihat')
  235. )
  236. );
  237. if ($include['jquery_ui'])
  238. {
  239. $bits['libraries']['ui'] = array('core', 'widget');
  240. }
  241. foreach ($tools as $tool)
  242. {
  243. // skip tools that are not available to the front-end
  244. if ($tool['info']['cp_only'] == 'y' && ! $cp_only)
  245. {
  246. continue;
  247. }
  248. // load the globals
  249. if (count($tool['globals']))
  250. {
  251. $tool['globals'] = $this->_prep_globals($tool['globals']);
  252. $bits['globals'] = array_merge_recursive($bits['globals'], $tool['globals']);
  253. }
  254. // load any libraries we need
  255. if ($tool['libraries'] && count($tool['libraries']))
  256. {
  257. $bits['libraries'] = array_merge_recursive($bits['libraries'], $tool['libraries']);
  258. }
  259. // add any styles we need
  260. if ( ! empty($tool['styles']))
  261. {
  262. $bits['styles'] .= $tool['styles'];
  263. }
  264. // load the definition
  265. if ( ! empty($tool['definition']))
  266. {
  267. $bits['definitions'] .= $tool['definition'];
  268. }
  269. // add to toolbar
  270. $bits['buttons'][] = strtolower(str_replace(' ', '_', $tool['info']['name']));
  271. }
  272. // potentially required assets
  273. $jquery = $this->EE->config->item('theme_folder_url') . 'javascript/' .
  274. ($this->EE->config->item('use_compressed_js') == 'n' ? 'src' : 'compressed') .
  275. '/jquery/jquery.js';
  276. $rtecss = $this->EE->config->item('theme_folder_url') . 'cp_themes/default/css/rte.css';
  277. $this->EE->load->library('javascript');
  278. // kick off the JS
  279. $js = '
  280. (function(){
  281. var EE = ' . $this->EE->javascript->generate_json($this->EE->javascript->global_vars) . ';' .
  282. '
  283. // make sure we have jQuery
  284. var interval = null;
  285. if (typeof jQuery === "undefined") {';
  286. if ($include['jquery'])
  287. {
  288. $js .= '
  289. var j = document.createElement("script");
  290. j.setAttribute("src","' . $jquery . '");
  291. document.getElementsByTagName("head")[0].appendChild(j);';
  292. }
  293. // Even if we don't load jQuery above, we still need to wait for it
  294. $js .= '
  295. interval = setInterval(loadRTE, 100);
  296. }
  297. else
  298. {
  299. loadRTE();
  300. }
  301. function loadRTE()
  302. {
  303. // make sure jQuery is loaded
  304. if ( typeof jQuery === "undefined" ){ return; }
  305. clearInterval( interval );
  306. var $ = jQuery;
  307. // RTE library
  308. ' . $this->_load_js_files($bits['libraries']) . '
  309. // RTE styles
  310. $("<link rel=\"stylesheet\" href=\"' . $rtecss . '\"/>")
  311. .add( $("<style>' . preg_replace( '/\\s+/', ' ', $bits['styles'] ) . '</style>"))
  312. .appendTo("head");
  313. // RTE globals
  314. ' . $this->_set_globals($bits['globals']) . '
  315. // RTE button class definitions
  316. ' . $bits['definitions'] . '
  317. // RTE editor setup for this page
  318. $("' . $selector . '")
  319. .addClass("WysiHat-field")
  320. .wysihat({
  321. buttons: '.$this->EE->javascript->generate_json($bits['buttons'], TRUE).'
  322. });
  323. }
  324. })();';
  325. return $js;
  326. }
  327. // ------------------------------------------------------------------------
  328. /**
  329. * Save RTE field
  330. *
  331. * Use to clean up RTE content prior to DB insertion
  332. *
  333. * @param string $data the RTE html content
  334. *
  335. * @return string the cleaned up RTE html content
  336. */
  337. public function save_field($data)
  338. {
  339. if ($this->EE->session->userdata('rte_enabled') != 'y'
  340. OR $this->EE->config->item('rte_enabled') != 'y')
  341. {
  342. return $data;
  343. }
  344. // If the editor was saved empty, save nothing to database
  345. // so it behaves as expected with conditional tags
  346. if ($this->is_empty($data))
  347. {
  348. return NULL;
  349. }
  350. $data = str_replace('<br>', "\n", $data); // must happen before the decode or we won't know which are ours
  351. $data = htmlspecialchars_decode($data, ENT_QUOTES);
  352. // decode double encoded code chunks
  353. if (preg_match_all("/\[code\](.+?)\[\/code\]/si", $data, $matches))
  354. {
  355. foreach ($matches[1] as $chunk)
  356. {
  357. $chunk = trim($chunk);
  358. $chunk = html_entity_decode($chunk, ENT_QUOTES, 'UTF-8');
  359. $data = str_replace($matches[0][$i], '[code]'.$chunk.'[/code]', $data);
  360. }
  361. }
  362. return $data;
  363. }
  364. // ------------------------------------------------------------------------
  365. /**
  366. * Display an RTE field
  367. *
  368. * @param string $data the RTE html content
  369. * @param string $field_name the field name for the RTE field
  370. * @param array $settings field settings:
  371. * field_ta_rows - the number of textarea rows
  372. * field_text_direction - ltr or rtl
  373. * field_fmt - xhtml, br or none
  374. *
  375. * @return string
  376. */
  377. public function display_field($data, $field_name, $settings)
  378. {
  379. $this->EE->load->helper('form');
  380. $field = array(
  381. 'name' => $field_name,
  382. 'id' => $field_name,
  383. 'rows' => $settings['field_ta_rows'],
  384. 'dir' => $settings['field_text_direction']
  385. );
  386. // form prepped nonsense
  387. $data = htmlspecialchars_decode($data, ENT_QUOTES);
  388. $code_marker = unique_marker('code');
  389. $code_chunks = array();
  390. $field_ft = isset($settings['field_fmt']) ? $settings['field_fmt'] : '';
  391. if ($field_ft == 'xhtml')
  392. {
  393. $data = trim($data);
  394. // Undo any existing newline formatting. Typography will change
  395. // it anyways and the rtf will add its own. Having this here
  396. // prevents growing-newline syndrome in the rtf and lets us switch
  397. // between rtf and non-rtf.
  398. $data = preg_replace("/<\/p>\n*<p>/is", "\n\n", $data);
  399. $data = preg_replace("/<br( \/)?>\n/is", "\n", $data);
  400. }
  401. // remove code chunks
  402. if (preg_match_all("/\[code\](.+?)\[\/code\]/si", $data, $matches))
  403. {
  404. foreach ($matches[1] as $i => $chunk)
  405. {
  406. $code_chunks[] = trim($chunk);
  407. $data = str_replace($matches[0][$i], $code_marker.$i, $data);
  408. }
  409. }
  410. // Check the RTE module and user's preferences
  411. if ($this->EE->session->userdata('rte_enabled') == 'y'
  412. AND $this->EE->config->item('rte_enabled') == 'y')
  413. {
  414. $field['class'] = 'WysiHat-field';
  415. foreach ($code_chunks as &$chunk)
  416. {
  417. $chunk = htmlentities($chunk, ENT_QUOTES, 'UTF-8');
  418. $chunk = str_replace("\n", '<br>', $chunk);
  419. }
  420. // xhtml vs br
  421. if ($settings['field_fmt'] == 'xhtml')
  422. {
  423. $this->EE->load->library('typography');
  424. $data = $this->EE->typography->_format_newlines($data);
  425. // Remove double paragraph tags
  426. $data = preg_replace("/(<\/?p>)\\1/is", "\\1", $data);
  427. }
  428. }
  429. // put code chunks back
  430. foreach ($code_chunks as $i => $chunk)
  431. {
  432. $data = str_replace($code_marker.$i, '[code]'.$chunk.'[/code]', $data);
  433. }
  434. // Swap {filedir_x} with the real URL. It will be converted back
  435. // upon submit by the RTE Image tool.
  436. $this->EE->load->model('file_upload_preferences_model');
  437. $dirs = $this->EE->file_upload_preferences_model->get_file_upload_preferences($this->EE->session->userdata('group_id'));
  438. foreach($dirs as $d)
  439. {
  440. // tag to replace
  441. $filedir = "{filedir_{$d['id']}}";
  442. $data = str_replace($filedir, $d['url'], $data);
  443. }
  444. $data = htmlspecialchars($data, ENT_QUOTES);
  445. $field['value'] = $data;
  446. return form_textarea($field);
  447. }
  448. // ------------------------------------------------------------------------
  449. /**
  450. * Check whether the specified data is empty html
  451. *
  452. * @param string $data the RTE html content
  453. *
  454. * @return bool
  455. */
  456. public function is_empty($data)
  457. {
  458. return in_array($data, $this->_empty);
  459. }
  460. // ------------------------------------------------------------------------
  461. /**
  462. * Loads JS library files
  463. *
  464. * Note: This is partially borrowed from the combo loader
  465. *
  466. * @access private
  467. * @param array $load A collection of JS libraries to load
  468. * @return string The libraries
  469. */
  470. private function _load_js_files($load = array())
  471. {
  472. $folder = $this->EE->config->item('use_compressed_js') == 'n' ? 'src' : 'compressed';
  473. if ( ! defined('PATH_JQUERY'))
  474. {
  475. define('PATH_JQUERY', PATH_THEMES.'javascript/'.$folder.'/jquery/');
  476. }
  477. $types = array(
  478. 'effect' => PATH_JQUERY.'ui/jquery.effects.',
  479. 'ui' => PATH_JQUERY.'ui/jquery.ui.',
  480. 'plugin' => PATH_JQUERY.'plugins/',
  481. 'file' => PATH_THEMES.'javascript/'.$folder.'/',
  482. 'package' => PATH_THIRD,
  483. 'fp_module' => PATH_MOD
  484. );
  485. $contents = '';
  486. foreach ($types as $type => $path)
  487. {
  488. if (isset($load[$type]))
  489. {
  490. // Don't load the same library twice
  491. $load[$type] = array_unique((array)$load[$type]);
  492. $files = $load[$type];
  493. if ( ! is_array($files))
  494. {
  495. $files = array( $files );
  496. }
  497. foreach ($files as $file)
  498. {
  499. if ($type == 'package' OR $type == 'fp_module')
  500. {
  501. $file = $file.'/javascript/'.$file;
  502. }
  503. elseif ($type == 'file')
  504. {
  505. $parts = explode('/', $file);
  506. $file = array();
  507. foreach ($parts as $part)
  508. {
  509. if ($part != '..')
  510. {
  511. $file[] = $this->EE->security->sanitize_filename($part);
  512. }
  513. }
  514. $file = implode('/', $file);
  515. }
  516. else
  517. {
  518. $file = $this->EE->security->sanitize_filename($file);
  519. }
  520. $file = $path.$file.'.js';
  521. if (file_exists($file))
  522. {
  523. $contents .= file_get_contents($file)."\n\n";
  524. }
  525. }
  526. }
  527. }
  528. return $contents;
  529. }
  530. // ------------------------------------------------------------------------
  531. /**
  532. * Prep global variables for JS
  533. *
  534. * @access private
  535. * @param array $globals The globals to load into JS
  536. * @return array the revised $globals array
  537. */
  538. private function _prep_globals($globals = array())
  539. {
  540. $temp = array();
  541. foreach ($globals as $key => $val)
  542. {
  543. if (strpos($key,'.') !== FALSE)
  544. {
  545. $parts = explode('.', $key);
  546. $parent = array_shift($parts);
  547. $key = implode('.', $parts);
  548. $temp[$parent] = $this->_prep_globals(array(
  549. $key => $val
  550. ));
  551. }
  552. else
  553. {
  554. $temp[$key] = $val;
  555. }
  556. }
  557. return $temp;
  558. }
  559. // ------------------------------------------------------------------------
  560. /**
  561. * Manage the assignment of global JS
  562. *
  563. * @access private
  564. * @param array $globals The globals to load into JS
  565. * @return string The JavaScript
  566. */
  567. private function _set_globals($globals = array())
  568. {
  569. $this->EE->load->library('javascript');
  570. $js = '';
  571. if (count($globals))
  572. {
  573. $js .= 'var EE = ' . $this->EE->javascript->generate_json($globals) . ';';
  574. }
  575. return $js;
  576. }
  577. }
  578. /* End of file rte_lib.php */
  579. /* Location: ./system/expressionengine/modules/rte/libraries/rte_lib.php */