PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/wp-content/plugins/broken-link-checker/modules/containers/custom_field.php

https://bitbucket.org/lgorence/quickpress
PHP | 580 lines | 295 code | 63 blank | 222 comment | 37 complexity | a4181b72e4b12be5b27c0dec3f9eb523 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1, AGPL-1.0
  1. <?php
  2. /*
  3. Plugin Name: Custom fields
  4. Description: Container module for post metadata.
  5. Version: 1.0
  6. Author: Janis Elsts
  7. ModuleID: custom_field
  8. ModuleCategory: container
  9. ModuleClassName: blcPostMetaManager
  10. */
  11. //Note : If it ever becomes necessary to check metadata on objects other than posts, it will
  12. //be fairly easy to extract a more general metadata container class from blcPostMeta.
  13. /**
  14. * blcPostMeta - A link container class for post metadata (AKA custom fields).
  15. *
  16. * Due to the way metadata works, this container differs significantly from other containers :
  17. * - container_field is equal to meta name, and container_id holds the ID of the post.
  18. * - There is one synch. record per post that determines the synch. state of all metadata fields of that post.
  19. * - Unlinking simply deletes the meta entry in question without involving the parser.
  20. * - The list of parse-able $fields is not fixed. Instead, it's initialized based on the
  21. * custom field list defined in Settings -> Link Checker.
  22. * - The $wrapped_object is an array (and isn't really used for anything).
  23. * - update_wrapped_object() does nothing.
  24. *
  25. * @package Broken Link Checker
  26. * @access public
  27. */
  28. class blcPostMeta extends blcContainer {
  29. var $meta_type = 'post';
  30. /**
  31. * Retrieve all metadata fields of the post associated with this container.
  32. * The results are cached in the internal $wrapped_object variable.
  33. *
  34. * @param bool $ensure_consistency
  35. * @return object The wrapped object.
  36. */
  37. function get_wrapped_object($ensure_consistency = false){
  38. if ( is_null($this->wrapped_object) || $ensure_consistency ) {
  39. $this->wrapped_object = get_metadata($this->meta_type, $this->container_id);
  40. }
  41. return $this->wrapped_object;
  42. }
  43. function update_wrapped_object(){
  44. trigger_error('Function blcPostMeta::update_wrapped_object() does nothing and should not be used.', E_USER_WARNING);
  45. }
  46. /**
  47. * Get the value of the specified metadata field of the object wrapped by this container.
  48. *
  49. * @access protected
  50. *
  51. * @param string $field Field name. If omitted, the value of the default field will be returned.
  52. * @return array
  53. */
  54. function get_field($field = ''){
  55. return get_metadata($this->meta_type, $this->container_id, $field);
  56. }
  57. /**
  58. * Update the value of the specified metadata field of the object wrapped by this container.
  59. *
  60. * @access protected
  61. *
  62. * @param string $field Meta name.
  63. * @param string $new_value New meta value.
  64. * @param string $old_value old meta value.
  65. * @return bool|WP_Error True on success, an error object if something went wrong.
  66. */
  67. function update_field($field, $new_value, $old_value = ''){
  68. $rez = update_metadata($this->meta_type, $this->container_id, $field, $new_value, $old_value);
  69. if ( $rez ){
  70. return true;
  71. } else {
  72. return new WP_Error(
  73. 'metadata_update_failed',
  74. sprintf(
  75. __("Failed to update the meta field '%s' on %s [%d]", 'broken-link-checker'),
  76. $field,
  77. $this->meta_type,
  78. $this->container_id
  79. )
  80. );
  81. }
  82. }
  83. /**
  84. * "Unlink"-ing a custom fields removes all metadata fields that contain the specified URL.
  85. *
  86. * @param string $field_name
  87. * @param blcParser $parser_type
  88. * @param string $url
  89. * @param string $raw_url
  90. * @return bool|WP_Error True on success, or an error object if something went wrong.
  91. */
  92. function unlink($field_name, $parser, $url, $raw_url =''){
  93. $rez = delete_metadata($this->meta_type, $this->container_id, $field_name, $raw_url);
  94. if ( $rez ){
  95. return true;
  96. } else {
  97. return new WP_Error(
  98. 'metadata_delete_failed',
  99. sprintf(
  100. __("Failed to delete the meta field '%s' on %s [%d]", 'broken-link-checker'),
  101. $field_name,
  102. $this->meta_type,
  103. $this->container_id
  104. )
  105. );
  106. }
  107. }
  108. /**
  109. * Change a meta field containing the specified URL to a new URL.
  110. *
  111. * @param string $field_name Meta name
  112. * @param blcParser $parser
  113. * @param string $new_url New URL.
  114. * @param string $old_url
  115. * @param string $old_raw_url Old meta value.
  116. * @return string|WP_Error The new value of raw_url on success, or an error object if something went wrong.
  117. */
  118. function edit_link($field_name, $parser, $new_url, $old_url = '', $old_raw_url = ''){
  119. /*
  120. FB::log(sprintf(
  121. 'Editing %s[%d]:%s - %s to %s',
  122. $this->container_type,
  123. $this->container_id,
  124. $field_name,
  125. $old_url,
  126. $new_url
  127. ));
  128. */
  129. if ( empty($old_raw_url) ){
  130. $old_raw_url = $old_url;
  131. }
  132. //Get the current values of the field that needs to be edited.
  133. //The default metadata parser ignores them, but we're still going
  134. //to set this argument to a valid value in case someone writes a
  135. //custom meta parser that needs it.
  136. $old_value = $this->get_field($field_name);
  137. //Get the new field value (a string).
  138. $edit_result = $parser->edit($old_value, $new_url, $old_url, $old_raw_url);
  139. if ( is_wp_error($edit_result) ){
  140. return $edit_result;
  141. }
  142. //Update the field with the new value returned by the parser.
  143. //Notice how $old_raw_url is usead instead of $old_value. $old_raw_url contains the entire old
  144. //value of the metadata field (see blcMetadataParser::parse()) and thus can be used to
  145. //differentiate between multiple meta fields with identical names.
  146. $update_result = $this->update_field( $field_name, $edit_result['content'], $old_raw_url );
  147. if ( is_wp_error($update_result) ){
  148. return $update_result;
  149. }
  150. //Return the new "raw" URL.
  151. return $edit_result['raw_url'];
  152. }
  153. /**
  154. * Get the default link text to use for links found in a specific container field.
  155. *
  156. * @param string $field
  157. * @return string
  158. */
  159. function default_link_text($field = ''){
  160. //Just use the field name. There's no way to know how the links inside custom fields are
  161. //used, so no way to know the "real" link text. Displaying the field name at least gives
  162. //the user a clue where to look if they want to find/modify the field.
  163. return $field;
  164. }
  165. function ui_get_source($container_field = '', $context = 'display'){
  166. $post_html = sprintf(
  167. '<a class="row-title" href="%s" title="%s">%s</a>',
  168. esc_url($this->get_edit_url()),
  169. esc_attr(__('Edit this post')),
  170. get_the_title($this->container_id)
  171. );
  172. return $post_html;
  173. }
  174. function ui_get_action_links($container_field){
  175. $actions = array();
  176. if ( current_user_can('edit_post', $this->container_id) ) {
  177. $actions['edit'] = '<span class="edit"><a href="' . $this->get_edit_url() . '" title="' . esc_attr(__('Edit this item')) . '">' . __('Edit') . '</a>';
  178. if ( $this->current_user_can_delete() ){
  179. if ( $this->can_be_trashed() ) {
  180. $actions['trash'] = sprintf(
  181. "<span><a class='submitdelete' title='%s' href='%s'>%s</a>",
  182. esc_attr(__('Move this item to the Trash')),
  183. get_delete_post_link($this->container_id, '', false),
  184. __('Trash')
  185. );
  186. } else {
  187. $actions['delete'] = sprintf(
  188. "<span><a class='submitdelete' title='%s' href='%s'>%s</a>",
  189. esc_attr(__('Delete this item permanently')),
  190. get_delete_post_link($this->container_id, '', true),
  191. __('Delete')
  192. );
  193. }
  194. }
  195. }
  196. $actions['view'] = '<span class="view"><a href="' . esc_url(get_permalink($this->container_id)) . '" title="' . esc_attr(sprintf(__('View "%s"', 'broken-link-checker'), get_the_title($this->container_id))) . '" rel="permalink">' . __('View') . '</a>';
  197. return $actions;
  198. }
  199. /**
  200. * Get edit URL for this container. Returns the URL of the Dashboard page where the item
  201. * associated with this container can be edited.
  202. *
  203. * @access protected
  204. *
  205. * @return string
  206. */
  207. function get_edit_url(){
  208. /*
  209. The below is a near-exact copy of the get_post_edit_link() function.
  210. Unfortunately we can't just call that function because it has a hardcoded
  211. caps-check which fails when called from the email notification script
  212. executed by Cron.
  213. */
  214. if ( !($post = &get_post( $this->container_id )) ){
  215. return '';
  216. }
  217. $context = 'display';
  218. //WP 3.0
  219. if ( 'display' == $context )
  220. $action = '&amp;action=edit';
  221. else
  222. $action = '&action=edit';
  223. $post_type_object = get_post_type_object( $post->post_type );
  224. if ( !$post_type_object ){
  225. return '';
  226. }
  227. return apply_filters( 'get_edit_post_link', admin_url( sprintf($post_type_object->_edit_link . $action, $post->ID) ), $post->ID, $context );
  228. }
  229. /**
  230. * Get the base URL of the container. For custom fields, the base URL is the permalink of
  231. * the post that the field is attached to.
  232. *
  233. * @return string
  234. */
  235. function base_url(){
  236. return get_permalink($this->container_id);
  237. }
  238. /**
  239. * Delete or trash the post corresponding to this container. If trash is enabled,
  240. * will always move the post to the trash instead of deleting.
  241. *
  242. * @return bool|WP_error
  243. */
  244. function delete_wrapped_object(){
  245. if ( EMPTY_TRASH_DAYS ){
  246. return $this->trash_wrapped_object();
  247. } else {
  248. if ( wp_delete_post($this->container_id) ){
  249. return true;
  250. } else {
  251. return new WP_Error(
  252. 'delete_failed',
  253. sprintf(
  254. __('Failed to delete post "%s" (%d)', 'broken-link-checker'),
  255. get_the_title($this->container_id),
  256. $this->container_id
  257. )
  258. );
  259. }
  260. }
  261. }
  262. /**
  263. * Move the post corresponding to this custom field to the Trash.
  264. *
  265. * @return bool|WP_Error
  266. */
  267. function trash_wrapped_object(){
  268. if ( !EMPTY_TRASH_DAYS ){
  269. return new WP_Error(
  270. 'trash_disabled',
  271. sprintf(
  272. __('Can\'t move post "%s" (%d) to the trash because the trash feature is disabled', 'broken-link-checker'),
  273. get_the_title($this->container_id),
  274. $this->container_id
  275. )
  276. );
  277. }
  278. $post = &get_post($this->container_id);
  279. if ( $post->post_status == 'trash' ){
  280. //Prevent conflicts between post and custom field containers trying to trash the same post.
  281. return true;
  282. }
  283. if ( wp_trash_post($this->container_id) ){
  284. return true;
  285. } else {
  286. return new WP_Error(
  287. 'trash_failed',
  288. sprintf(
  289. __('Failed to move post "%s" (%d) to the trash', 'broken-link-checker'),
  290. get_the_title($this->container_id),
  291. $this->container_id
  292. )
  293. );
  294. }
  295. }
  296. function current_user_can_delete(){
  297. $post = &get_post($this->container_id);
  298. $post_type_object = get_post_type_object($post->post_type);
  299. return current_user_can( $post_type_object->cap->delete_post, $this->container_id );
  300. }
  301. function can_be_trashed(){
  302. return defined('EMPTY_TRASH_DAYS') && EMPTY_TRASH_DAYS;
  303. }
  304. }
  305. class blcPostMetaManager extends blcContainerManager {
  306. var $container_class_name = 'blcPostMeta';
  307. var $meta_type = 'post';
  308. function init(){
  309. parent::init();
  310. //Intercept 2.9+ style metadata modification actions
  311. add_action( "added_{$this->meta_type}_meta", array(&$this, 'meta_modified'), 10, 4 );
  312. add_action( "updated_{$this->meta_type}_meta", array(&$this, 'meta_modified'), 10, 4 );
  313. add_action( "deleted_{$this->meta_type}_meta", array(&$this, 'meta_modified'), 10, 4 );
  314. //Also intercept the equivalent actions used in /wp-admin/includes/post.php.
  315. //(WP is bloody inconsistent. The action names differ by a single character
  316. //but have different argument counts)
  317. add_action( "added_{$this->meta_type}meta", array(&$this, 'meta_modified'), 10, 4 );
  318. add_action( "deleted_{$this->meta_type}meta", array(&$this, 'meta_modified'), 10, 1 );//NB : 1 argument!
  319. //When a post is deleted, also delete the custom field container associated with it.
  320. add_action('delete_post', array(&$this,'post_deleted'));
  321. add_action('trash_post', array(&$this,'post_deleted'));
  322. //Re-parse custom fields when a post is restored from trash
  323. add_action('untrashed_post', array(&$this,'post_untrashed'));
  324. }
  325. /**
  326. * Get a list of parseable fields.
  327. *
  328. * @return array
  329. */
  330. function get_parseable_fields(){
  331. //Fields = custom field names as entered by the user.
  332. $fields = array();
  333. if ( is_array($this->plugin_conf->options['custom_fields']) ){
  334. foreach($this->plugin_conf->options['custom_fields'] as $meta_name){
  335. $fields[$meta_name] = 'metadata';
  336. }
  337. }
  338. return $fields;
  339. }
  340. /**
  341. * Instantiate multiple containers of the container type managed by this class.
  342. *
  343. * @param array $containers Array of assoc. arrays containing container data.
  344. * @param string $purpose An optional code indicating how the retrieved containers will be used.
  345. * @param bool $load_wrapped_objects Preload wrapped objects regardless of purpose.
  346. *
  347. * @return array of blcPostMeta indexed by "container_type|container_id"
  348. */
  349. function get_containers($containers, $purpose = '', $load_wrapped_objects = false){
  350. $containers = $this->make_containers($containers);
  351. /*
  352. When links from custom fields are displayed in Tools -> Broken Links,
  353. each one also shows the title of the post that the custom field(s)
  354. belong to. Thus it makes sense to pre-cache the posts beforehand - it's
  355. faster to load them all at once than to make a separate query for each
  356. one later.
  357. So make a list of involved post IDs and load them.
  358. Calling get_posts() will automatically populate the post cache, so we
  359. don't need to actually store the results anywhere in the container object().
  360. */
  361. $preload = $load_wrapped_objects || in_array($purpose, array(BLC_FOR_DISPLAY));
  362. if ( $preload ){
  363. $post_ids = array();
  364. foreach($containers as $container){
  365. $post_ids[] = $container->container_id;
  366. }
  367. $args = array('include' => implode(',', $post_ids));
  368. get_posts($args);
  369. }
  370. return $containers;
  371. }
  372. /**
  373. * Create or update synchronization records for all containers managed by this class.
  374. *
  375. * @param bool $forced If true, assume that all synch. records are gone and will need to be recreated from scratch.
  376. * @return void
  377. */
  378. function resynch($forced = false){
  379. global $wpdb; /** @var wpdb $wpdb */
  380. global $blclog;
  381. if ( $forced ){
  382. //Create new synchronization records for all posts.
  383. $blclog->log('...... Creating synch records for all custom fields');
  384. $q = "INSERT INTO {$wpdb->prefix}blc_synch(container_id, container_type, synched)
  385. SELECT id, '{$this->container_type}', 0
  386. FROM {$wpdb->posts}
  387. WHERE
  388. {$wpdb->posts}.post_status = 'publish'
  389. AND {$wpdb->posts}.post_type IN ('post', 'page')";
  390. $wpdb->query( $q );
  391. $blclog->log(sprintf('...... %d rows affected', $wpdb->rows_affected));
  392. } else {
  393. //Delete synch records corresponding to posts that no longer exist.
  394. $blclog->log('...... Deleting custom field synch records corresponding to deleted posts');
  395. $q = "DELETE synch.*
  396. FROM
  397. {$wpdb->prefix}blc_synch AS synch LEFT JOIN {$wpdb->posts} AS posts
  398. ON posts.ID = synch.container_id
  399. WHERE
  400. synch.container_type = '{$this->container_type}' AND posts.ID IS NULL";
  401. $wpdb->query( $q );
  402. $blclog->log(sprintf('...... %d rows affected', $wpdb->rows_affected));
  403. //Remove the 'synched' flag from all posts that have been updated
  404. //since the last time they were parsed/synchronized.
  405. $blclog->log('...... Marking custom fields on changed post as unsynched');
  406. $q = "UPDATE
  407. {$wpdb->prefix}blc_synch AS synch
  408. JOIN {$wpdb->posts} AS posts ON (synch.container_id = posts.ID and synch.container_type='{$this->container_type}')
  409. SET
  410. synched = 0
  411. WHERE
  412. synch.last_synch < posts.post_modified";
  413. $wpdb->query( $q );
  414. $blclog->log(sprintf('...... %d rows affected', $wpdb->rows_affected));
  415. //Create synch. records for posts that don't have them.
  416. $blclog->log('...... Creating custom field synch records for new posts');
  417. $q = "INSERT INTO {$wpdb->prefix}blc_synch(container_id, container_type, synched)
  418. SELECT id, '{$this->container_type}', 0
  419. FROM
  420. {$wpdb->posts} AS posts LEFT JOIN {$wpdb->prefix}blc_synch AS synch
  421. ON (synch.container_id = posts.ID and synch.container_type='{$this->container_type}')
  422. WHERE
  423. posts.post_status = 'publish'
  424. AND posts.post_type IN ('post', 'page')
  425. AND synch.container_id IS NULL";
  426. $wpdb->query($q);
  427. $blclog->log(sprintf('...... %d rows affected', $wpdb->rows_affected));
  428. }
  429. }
  430. /**
  431. * Mark custom fields as unsynched when they're modified or deleted.
  432. *
  433. * @param array|int $meta_id
  434. * @param int $object_id
  435. * @param string $meta_key
  436. * @param string $meta_value
  437. * @return void
  438. */
  439. function meta_modified($meta_id, $object_id = 0, $meta_key= '', $meta_value = ''){
  440. global $wpdb; /** @var wpdb $wpdb */
  441. //If object_id isn't specified then the hook was probably called from the
  442. //stupidly inconsistent delete_meta() function in /wp-admin/includes/post.php.
  443. if ( empty($object_id) ){
  444. //We must manually retrieve object_id and meta_key from the DB.
  445. if ( is_array($meta_id) ){
  446. $meta_id = array_shift($meta_id);
  447. }
  448. $meta = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $wpdb->postmeta WHERE meta_id = %d", $meta_id), ARRAY_A );
  449. if ( empty($meta) ){
  450. return;
  451. }
  452. $object_id = $meta['post_id'];
  453. $meta_key = $meta['meta_key'];
  454. }
  455. //Metadata changes only matter to us if the modified key
  456. //is one that the user wants checked.
  457. $conf = blc_get_configuration();
  458. if ( !is_array($conf->options['custom_fields']) ){
  459. return;
  460. }
  461. if ( !in_array($meta_key, $conf->options['custom_fields']) ){
  462. return;
  463. }
  464. $container = blcContainerHelper::get_container( array($this->container_type, intval($object_id)) );
  465. $container->mark_as_unsynched();
  466. }
  467. /**
  468. * Delete custom field synch. records when the post that they belong to is deleted.
  469. *
  470. * @param int $post_id
  471. * @return void
  472. */
  473. function post_deleted($post_id){
  474. //Get the associated container object
  475. $container = blcContainerHelper::get_container( array($this->container_type, intval($post_id)) );
  476. //Delete it
  477. $container->delete();
  478. //Clean up any dangling links
  479. blc_cleanup_links();
  480. }
  481. /**
  482. * When a post is restored, mark all of its custom fields as unparsed.
  483. * Called via the 'untrashed_post' action.
  484. *
  485. * @param int $post_id
  486. * @return void
  487. */
  488. function post_untrashed($post_id){
  489. //Get the associated container object
  490. $container = blcContainerHelper::get_container( array($this->container_type, intval($post_id)) );
  491. $container->mark_as_unsynched();
  492. }
  493. /**
  494. * Get the message to display after $n posts have been deleted.
  495. *
  496. * @uses blcAnyPostContainerManager::ui_bulk_delete_message()
  497. *
  498. * @param int $n Number of deleted posts.
  499. * @return string A delete confirmation message, e.g. "5 posts were moved to the trash"
  500. */
  501. function ui_bulk_delete_message($n){
  502. return blcAnyPostContainerManager::ui_bulk_delete_message($n);
  503. }
  504. /**
  505. * Get the message to display after $n posts have been trashed.
  506. *
  507. * @param int $n Number of deleted posts.
  508. * @return string A confirmation message, e.g. "5 posts were moved to trash"
  509. */
  510. function ui_bulk_trash_message($n){
  511. return blcAnyPostContainerManager::ui_bulk_trash_message($n);
  512. }
  513. }
  514. ?>