diff --git a/attachment/attachment.php b/attachment/attachment.php index 412376c16..cdecc2a82 100644 --- a/attachment/attachment.php +++ b/attachment/attachment.php @@ -95,7 +95,7 @@ protected function configure_properties() 'physical_filename' => array('default' => '', 'max' => 255), 'attachment_directory' => array('default' => '', 'max' => 255), 'real_filename' => array('default' => '', 'max' => 255), - 'attachment_comment' => array('default' => ''), + 'attachment_comment' => array('default' => '', 'encode_ucr' => true), 'download_count' => array('default' => 0), @@ -202,6 +202,14 @@ public function submit($data = null) if (!empty($data)) { + foreach ($data as $name => $value) + { + if (isset($this->object_config[$name])) + { + $data[$name] = $this->validate_property($value, $this->object_config[$name]); + } + } + $sql = 'UPDATE ' . $this->sql_table . ' SET ' . $this->db->sql_build_array('UPDATE', $data) . ' WHERE attachment_id = ' . $this->get_id(); diff --git a/attachment/uploader.php b/attachment/uploader.php index d22820f02..067e8f72e 100644 --- a/attachment/uploader.php +++ b/attachment/uploader.php @@ -16,6 +16,7 @@ use phpbb\files\upload; use phpbb\request\request_interface; use phpbb\titania\access; +use phpbb\titania\unicode; use phpbb\titania\ext; class uploader @@ -260,6 +261,16 @@ public function upload_file() return false; } + if (unicode::contains_unsupported($file->get('uploadname'))) + { + $file->error[] = $this->user->lang('INVALID_FILENAME', $file->get('uploadname')); + $file->remove(); + $this->filedata['error'] = array_merge($this->filedata['error'], $file->error); + $this->filedata['post_attach'] = false; + + return false; + } + // Set max file size for anyone but team members. if (!$this->access->is_team()) { diff --git a/contribution/extension/type.php b/contribution/extension/type.php index 2d93f7398..7e8eb2040 100644 --- a/contribution/extension/type.php +++ b/contribution/extension/type.php @@ -18,6 +18,7 @@ use phpbb\titania\attachment\attachment; use phpbb\titania\config\config as ext_config; use phpbb\titania\contribution\type\base; +use phpbb\titania\unicode; use phpbb\titania\entity\package; use phpbb\user; @@ -216,7 +217,7 @@ public function epv_test(\titania_contribution $contrib, \titania_revision $revi public function validate_ext_name($name) { return (bool) preg_match( - '#^[a-zA-Z0-9\x7f-\xff]{2,}/[a-zA-Z0-9\x7f-\xff]{2,}$#', + '#^[a-zA-Z0-9\x{007F}-\x{FFFF}]{2,}/[a-zA-Z0-9\x{007F}-\x{FFFF}]{2,}$#u', $name ); } @@ -283,10 +284,10 @@ protected function repack(package $package, \titania_contribution $contrib, \tit $data = $this->update_phpbb_requirement($data, $revision); $data = $this->set_version_check($data, $contrib); - $data = json_encode( + $data = unicode::escape_json(json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE - ); + )); file_put_contents($composer_file, $data); $package->restore_root($ext_base_path, $ext_name); diff --git a/contribution/style/colorizeit_helper.php b/contribution/style/colorizeit_helper.php index 6ee21deb5..6bef945eb 100644 --- a/contribution/style/colorizeit_helper.php +++ b/contribution/style/colorizeit_helper.php @@ -13,6 +13,8 @@ namespace phpbb\titania\contribution\style; +use phpbb\titania\unicode; + class colorizeit_helper { /** @var array */ @@ -69,6 +71,14 @@ public function generate_options($zip_file, $temp_dir) public function submit_options($options, $revision_id, $db) { $options = serialize($options); + + // These values come from configuration files inside the uploaded style. + // Keep serving unsupported names without attempting to cache them. + if (unicode::contains_unsupported($options)) + { + return; + } + $sql = 'UPDATE ' . TITANIA_REVISIONS_TABLE . ' SET revision_clr_options = "' . $db->sql_escape($options) . '" WHERE revision_id = ' . (int) $revision_id; diff --git a/contribution/style/type.php b/contribution/style/type.php index ff9385281..81db1e702 100644 --- a/contribution/style/type.php +++ b/contribution/style/type.php @@ -19,6 +19,7 @@ use phpbb\titania\attachment\attachment; use phpbb\titania\config\config as ext_config; use phpbb\titania\contribution\type\base; +use phpbb\titania\unicode; use phpbb\titania\entity\package; use phpbb\user; @@ -165,7 +166,7 @@ public function upload_check(attachment $attachment) public function fix_package_name(\titania_contribution $contrib, \titania_revision $revision, attachment $attachment, $root_dir = null) { // If we managed to find a single parent directory, then we use that in the zip name, otherwise we fall back to using contrib_name_clean - if ($root_dir !== null) + if ($root_dir !== null && !unicode::contains_unsupported($root_dir)) { $new_real_filename = $root_dir . '_' . strtolower($revision->revision_version) . '.' . $attachment->extension; } diff --git a/contribution/translation/type.php b/contribution/translation/type.php index fdf5cbbf0..1b0b1b5b1 100644 --- a/contribution/translation/type.php +++ b/contribution/translation/type.php @@ -173,7 +173,7 @@ public function translation_validate(\titania_contribution $contrib, \titania_re // Save the translation validation results (we need to save it here so that we can add it to the post later) $queue = $revision->get_queue(); - $queue->tv_results = $translation_validator_output; + $queue->tv_results = utf8_encode_ucr($translation_validator_output); $queue->submit(); return array( diff --git a/controller/author.php b/controller/author.php index a8d0267d5..9219a5010 100644 --- a/controller/author.php +++ b/controller/author.php @@ -405,6 +405,11 @@ protected function create() )); $authors['author'] = array($this->user->data['username'] => $this->user->data['user_id']); + if ($contrib->contrib_name_clean === '') + { + $contrib->generate_permalink(); + } + $error = $contrib->validate($settings['categories'], $authors, $settings['custom']); if (($form_key_error = $message->validate_form_key()) !== false) diff --git a/controller/contribution/manage.php b/controller/contribution/manage.php index b52ce9eb7..453c3f4d1 100644 --- a/controller/contribution/manage.php +++ b/controller/contribution/manage.php @@ -194,6 +194,15 @@ public function manage($contrib_type, $contrib) $this->contrib->post_data($this->message); + if (!$this->is_moderator) + { + $this->settings['permalink'] = $this->contrib->contrib_name_clean; + } + else if ($this->settings['permalink'] === '') + { + $this->settings['permalink'] = $this->contrib->get_generated_permalink(); + } + $authors = $this->contrib->get_authors_from_usernames(array( 'active_coauthors' => $this->settings['coauthors']['active'], 'nonactive_coauthors' => $this->settings['coauthors']['nonactive'], @@ -206,7 +215,8 @@ public function manage($contrib_type, $contrib) $this->settings['categories'], $authors, $this->settings['custom'], - $this->contrib->contrib_name_clean + $this->contrib->contrib_name_clean, + $this->settings['permalink'] )); // Did we succeed or have an error? @@ -474,11 +484,6 @@ protected function submit($authors, $old_settings) if ($this->settings['permalink'] != $this->contrib->contrib_name_clean) { - if ($this->settings['permalink'] == '') - { - $this->contrib->generate_permalink(); - $this->settings['permalink'] = $this->contrib->contrib_name_clean; - } $this->contrib->change_permalink($this->settings['permalink']); } } diff --git a/controller/contribution/revision.php b/controller/contribution/revision.php index f163ccd4a..72a8bb7ea 100644 --- a/controller/contribution/revision.php +++ b/controller/contribution/revision.php @@ -15,6 +15,7 @@ use phpbb\titania\composer\repository; use phpbb\titania\contribution\type\collection as type_collection; +use phpbb\titania\unicode; use phpbb\titania\ext; use Symfony\Component\HttpFoundation\JsonResponse; @@ -589,7 +590,7 @@ protected function create_queue_item($allow_repack, $test_account) { $this->queue->queue_notes .= "\n\n[b]" . $this->user->lang('TEST_ACCOUNT') . "[/b]\n" . - $test_account + utf8_encode_ucr($test_account) ; } $this->queue->submit(); @@ -615,6 +616,20 @@ protected function validate_settings($settings) $error[] = $this->user->lang['NO_REVISION_VERSION']; } + $metadata = array_merge( + array($settings['name'], $settings['version'], $settings['license']), + $settings['custom'] + ); + + foreach ($metadata as $value) + { + if (is_string($value) && unicode::contains_unsupported($value)) + { + $error[] = $this->user->lang['REVISION_EMOJI_NOT_ALLOWED']; + break; + } + } + if (!empty($this->contrib->type->license_options) && !$this->contrib->type->license_allow_custom && !in_array($settings['license'], $this->contrib->type->license_options)) { diff --git a/controller/contribution/revision_edit.php b/controller/contribution/revision_edit.php index 05d7107c7..d4b53a42a 100644 --- a/controller/contribution/revision_edit.php +++ b/controller/contribution/revision_edit.php @@ -14,6 +14,7 @@ namespace phpbb\titania\controller\contribution; use phpbb\titania\contribution\type\collection as type_collection; +use phpbb\titania\unicode; use phpbb\titania\ext; use Symfony\Component\HttpFoundation\JsonResponse; @@ -243,6 +244,16 @@ protected function validate_settings($settings) { $error[] = $this->user->lang['FORM_INVALID']; } + + if ( + unicode::contains_unsupported($settings['name']) || + unicode::contains_unsupported($settings['license']) || + unicode::contains_unsupported($settings['custom_license']) + ) + { + $error[] = $this->user->lang['REVISION_EMOJI_NOT_ALLOWED']; + } + $license_options = ($this->contrib->type->license_options) ?: array(); if ($license_options && !$this->contrib->type->license_allow_custom && !in_array($settings['license'], $license_options)) diff --git a/entity/database_base.php b/entity/database_base.php index b5323fb8c..f23cf470b 100644 --- a/entity/database_base.php +++ b/entity/database_base.php @@ -255,7 +255,21 @@ protected function validate_string($value, $config) // Truncate to the maximum length if (isset($config['max']) && $config['max']) { - truncate_string($value, $config['max']); + $value = truncate_string($value, $config['max']); + } + + if (!empty($config['encode_ucr'])) + { + $value = utf8_encode_ucr($value); + + // Numeric references use more storage characters than the + // characters they represent. + while (!empty($config['max']) && utf8_strlen($value) > $config['max']) + { + $length = utf8_strlen(html_entity_decode($value, ENT_COMPAT, 'UTF-8')); + $value = truncate_string($value, $length - 1); + $value = utf8_encode_ucr($value); + } } return $value; diff --git a/includes/objects/attention.php b/includes/objects/attention.php index 6c8b8832d..a7398e087 100644 --- a/includes/objects/attention.php +++ b/includes/objects/attention.php @@ -66,8 +66,8 @@ public function __construct() 'attention_time' => array('default' => titania::$time), 'attention_close_time' => array('default' => 0), 'attention_close_user' => array('default' => 0), - 'attention_title' => array('default' => ''), - 'attention_description' => array('default' => ''), + 'attention_title' => array('default' => '', 'max' => 255, 'encode_ucr' => true), + 'attention_description' => array('default' => '', 'encode_ucr' => true), 'notify_reporter' => array('default' => 0), )); diff --git a/includes/objects/category.php b/includes/objects/category.php index 0062566ae..aae5372b3 100644 --- a/includes/objects/category.php +++ b/includes/objects/category.php @@ -11,6 +11,7 @@ * */ +use phpbb\titania\unicode; use phpbb\titania\ext; use phpbb\titania\sync; @@ -702,6 +703,10 @@ public function validate() { $error[] = phpbb::$user->lang['NO_CATEGORY_NAME']; } + else if (unicode::contains_unsupported($this->category_name)) + { + $error[] = phpbb::$user->lang['CATEGORY_EMOJI_NOT_ALLOWED']; + } if (!$this->category_name_clean || !preg_match('/^[a-zA-Z0-9\-\_]+$/', $this->category_name_clean)) { diff --git a/includes/objects/contribution.php b/includes/objects/contribution.php index 47d45a10e..fd7c26e0f 100644 --- a/includes/objects/contribution.php +++ b/includes/objects/contribution.php @@ -15,6 +15,7 @@ use phpbb\titania\composer\repository; use phpbb\titania\contribution\type\collection as type_collection; use phpbb\titania\contribution\type\type_interface; +use phpbb\titania\unicode; use phpbb\titania\ext; use phpbb\titania\message\message; use phpbb\titania\url\url; @@ -1446,7 +1447,6 @@ public function change_status($new_status) public function change_permalink($new_permalink) { $old_permalink = $this->contrib_name_clean; - $new_permalink = url::generate_slug($new_permalink); if ($this->validate_permalink($new_permalink, $old_permalink)) { @@ -1555,10 +1555,11 @@ public function change_permalink($new_permalink) array('active_coauthors' => array(username => username)). * @param array $custom_fields Custom field values. * @param string $old_permalink Old permalink. Defaults to empty string. + * @param string|null $new_permalink Submitted permalink. Defaults to the value on the entity. * * @return array Returns array containing any errors found. */ - public function validate($contrib_categories, $authors, $custom_fields, $old_permalink = '') + public function validate($contrib_categories, $authors, $custom_fields, $old_permalink = '', $new_permalink = null) { phpbb::$user->add_lang('ucp'); @@ -1569,6 +1570,32 @@ public function validate($contrib_categories, $authors, $custom_fields, $old_per $error[] = phpbb::$user->lang['EMPTY_CONTRIB_NAME']; } + $metadata = array( + $this->contrib_name, + $new_permalink !== null ? $new_permalink : $this->contrib_name_clean, + ); + $metadata = array_merge($metadata, $custom_fields); + + $demos = json_decode($this->contrib_demo, true); + if (is_array($demos)) + { + // JSON encoding can otherwise hide unsupported characters behind + // surrogate escapes. + $metadata['contrib_demo'] = implode("\n", $demos); + } + + $metadata_has_unsupported = false; + + foreach ($metadata as $value) + { + if (is_string($value) && unicode::contains_unsupported($value, false)) + { + $metadata_has_unsupported = true; + $error[] = phpbb::$user->lang['CONTRIB_EMOJI_NOT_ALLOWED']; + break; + } + } + if (!$this->contrib_type) { $error[] = phpbb::$user->lang['EMPTY_CONTRIB_TYPE']; @@ -1602,13 +1629,8 @@ public function validate($contrib_categories, $authors, $custom_fields, $old_per $this->set_type($this->contrib_type); $error = array_merge($error, $this->type->validate_contrib_fields($custom_fields)); - if (!$this->contrib_name_clean) - { - // If they leave it blank automatically create it - $this->generate_permalink(); - } - - if (($permalink_error = $this->validate_permalink($this->contrib_name_clean, $old_permalink)) !== false) + $permalink = $new_permalink !== null ? $new_permalink : $this->contrib_name_clean; + if (!$metadata_has_unsupported && ($permalink_error = $this->validate_permalink($permalink, $old_permalink)) !== false) { $error[] = $permalink_error; } @@ -1703,7 +1725,17 @@ public function validate($contrib_categories, $authors, $custom_fields, $old_per */ public function generate_permalink() { - $clean_name = url::generate_slug($this->contrib_name); + $this->contrib_name_clean = $this->get_generated_permalink(); + } + + /** + * Generate an available contribution permalink. + * + * @return string + */ + public function get_generated_permalink() + { + $clean_name = $this->generate_permalink_slug($this->contrib_name); $append = ''; $i = 2; while ($this->permalink_exists($clean_name . $append)) @@ -1711,7 +1743,27 @@ public function generate_permalink() $append = '_' . $i; $i++; } - $this->contrib_name_clean = $clean_name . $append; + + return $clean_name . $append; + } + + /** + * Generate a contribution permalink containing Unicode letters, combining + * marks, numbers, and underscores. + * + * @param string $value + * @return string + */ + protected function generate_permalink_slug($value) + { + // Variation selectors control the presentation of the preceding + // character. They are combining marks, but have no place in a + // permalink and can otherwise survive after an emoji is removed. + $value = preg_replace('/[\x{FE00}-\x{FE0F}\x{E0100}-\x{E01EF}]/u', '', $value); + + // Preserve repeated and surrounding underscores for compatibility with + // existing permalink rules. + return preg_replace('/[^\p{L}\p{M}\p{N}_]+/u', '_', url::generate_slug($value)); } /* @@ -1724,14 +1776,26 @@ public function generate_permalink() */ public function validate_permalink($permalink, $old_permalink) { - if (url::generate_slug($permalink) !== $permalink) + // Preserve existing Unicode permalinks until they are intentionally changed. + if ($permalink !== '' && $permalink === $old_permalink) + { + return false; + } + + $generated_permalink = $this->generate_permalink_slug($permalink); + if ($generated_permalink !== $permalink) + { + return phpbb::$user->lang('INVALID_PERMALINK', $generated_permalink); + } + + if ($permalink === '') { - return phpbb::$user->lang('INVALID_PERMALINK', url::generate_slug($permalink)); + return phpbb::$user->lang['EMPTY_CONTRIB_PERMALINK']; } - if ($permalink === '' || ($permalink !== $old_permalink && $this->permalink_exists($permalink))) + if ($permalink !== $old_permalink && $this->permalink_exists($permalink)) { - return phpbb::$user->lang['CONTRIB_NAME_EXISTS']; + return phpbb::$user->lang['CONTRIB_PERMALINK_EXISTS']; } return false; @@ -1748,7 +1812,8 @@ public function permalink_exists($permalink) $sql = 'SELECT contrib_id FROM ' . $this->sql_table . " WHERE contrib_name_clean = '" . phpbb::$db->sql_escape($permalink) . "' - AND contrib_type = " . (int) $this->contrib_type; + AND contrib_type = " . (int) $this->contrib_type . ' + AND contrib_id <> ' . (int) $this->contrib_id; $result = phpbb::$db->sql_query($sql); $contrib_id = phpbb::$db->sql_fetchfield('contrib_id'); phpbb::$db->sql_freeresult($result); diff --git a/includes/objects/faq.php b/includes/objects/faq.php index 1a18939c3..3fcd954a4 100644 --- a/includes/objects/faq.php +++ b/includes/objects/faq.php @@ -62,7 +62,7 @@ public function __construct($faq_id = false) $this->object_config = array_merge($this->object_config, array( 'faq_id' => array('default' => 0), 'contrib_id' => array('default' => 0), - 'faq_subject' => array('default' => '', 'message_field' => 'subject', 'max' => 255), + 'faq_subject' => array('default' => '', 'message_field' => 'subject', 'max' => 255, 'encode_ucr' => true), 'faq_text' => array('default' => '', 'message_field' => 'message'), 'faq_text_bitfield' => array('default' => '', 'message_field' => 'message_bitfield'), 'faq_text_uid' => array('default' => '', 'message_field' => 'message_uid'), diff --git a/includes/objects/post.php b/includes/objects/post.php index a5dbc8391..15a0b1742 100644 --- a/includes/objects/post.php +++ b/includes/objects/post.php @@ -108,10 +108,10 @@ public function __construct($type = ext::TITANIA_SUPPORT, $topic = false, $post_ 'post_edit_time' => array('default' => 0), // The last time that user edit the post 'post_edit_user' => array('default' => 0), // The last user to edit the post - 'post_edit_reason' => array('default' => ''), // Reason for deleting/editing + 'post_edit_reason' => array('default' => '', 'max' => 255, 'encode_ucr' => true), // Reason for deleting/editing 'post_delete_user' => array('default' => 0), // The last user to delete the post - 'post_subject' => array('default' => '', 'message_field' => 'subject', 'max' => 255), + 'post_subject' => array('default' => '', 'message_field' => 'subject', 'max' => 255, 'encode_ucr' => true), 'post_text' => array('default' => '', 'message_field' => 'message'), 'post_text_bitfield' => array('default' => '', 'message_field' => 'message_bitfield'), 'post_text_uid' => array('default' => '', 'message_field' => 'message_uid'), diff --git a/includes/objects/queue.php b/includes/objects/queue.php index 89241ea22..01371ea7a 100644 --- a/includes/objects/queue.php +++ b/includes/objects/queue.php @@ -216,13 +216,13 @@ public function update_first_queue_post($post_subject = false) phpbb::$config['min_post_chars'] = 1; phpbb::$config['max_post_chars'] = 0; - $this->forum_queue_update_first_queue_post($post); - // Store the post $post->generate_text_for_storage(true, true, true); $post->submit(); $this->queue_topic_id = $post->topic_id; + + $this->forum_queue_update_first_queue_post($post); } /** @@ -826,8 +826,6 @@ protected function forum_queue_update_first_queue_post(&$post_object) return; } - $post_object->submit(); - titania::_include('functions_posting', 'phpbb_posting'); // Need some stuff @@ -874,10 +872,11 @@ protected function forum_queue_update_first_queue_post(&$post_object) get_formatted_filesize($download['filesize']) ); - $post_text .= "\n\n" . $post_object->post_text; + $queue_post_text = $post_object->post_text; + handle_queue_attachments($post_object, $queue_post_text); + message::decode($queue_post_text, $post_object->post_text_uid); - handle_queue_attachments($post_object, $post_text); - message::decode($post_text, $post_object->post_text_uid); + $post_text .= "\n\n" . $queue_post_text; $post_text .= "\n\n" . $path_helper->strip_url_params($post_object->get_url(), 'sid'); diff --git a/includes/objects/topic.php b/includes/objects/topic.php index 208e091fc..ef0985d8a 100644 --- a/includes/objects/topic.php +++ b/includes/objects/topic.php @@ -89,8 +89,8 @@ public function __construct($topic_id = 0) 'topic_posts' => array('default' => ''), // Post count; separated by : between access levels ('10:9:8' = 10 team; 9 Mod Author; 8 Public) 'topic_views' => array('default' => 0), // View count - 'topic_subject' => array('default' => ''), - 'topic_subject_clean' => array('default' => ''), + 'topic_subject' => array('default' => '', 'max' => 255, 'encode_ucr' => true), + 'topic_subject_clean' => array('default' => '', 'max' => 255, 'encode_ucr' => true), 'topic_first_post_id' => array('default' => 0), 'topic_first_post_user_id' => array('default' => 0), @@ -103,7 +103,7 @@ public function __construct($topic_id = 0) 'topic_last_post_username' => array('default' => ''), 'topic_last_post_user_colour' => array('default' => ''), 'topic_last_post_time' => array('default' => (int) titania::$time), - 'topic_last_post_subject' => array('default' => ''), + 'topic_last_post_subject' => array('default' => '', 'max' => 255, 'encode_ucr' => true), 'phpbb_topic_id' => array('default' => 0), )); diff --git a/language/en/common.php b/language/en/common.php index e1a985e3b..751cd1c68 100644 --- a/language/en/common.php +++ b/language/en/common.php @@ -63,6 +63,7 @@ 'CATEGORY_DELETED' => 'Category Deleted', 'CATEGORY_DESC' => 'Category Description', 'CATEGORY_DUPLICATE_PARENT' => 'Category cannot be its own parent.', + 'CATEGORY_EMOJI_NOT_ALLOWED'=> 'Four-byte characters are not allowed in category names.', 'CATEGORY_HAS_CHILDREN' => 'This category cannot be deleted because it contains children categories.', 'CATEGORY_INFORMATION' => 'Category Information', 'CATEGORY_NAME' => 'Category Name', diff --git a/language/en/contributions.php b/language/en/contributions.php index 35c9bdca8..3de7f78fc 100644 --- a/language/en/contributions.php +++ b/language/en/contributions.php @@ -72,19 +72,20 @@ 'CONTRIB_DISABLED' => 'Hidden + Disabled', 'CONTRIB_DOWNLOAD_DISABLED' => 'Downloads Disabled', 'CONTRIB_EDITED' => 'The contribution has been successfully edited.', + 'CONTRIB_EMOJI_NOT_ALLOWED' => 'Four-byte characters are not allowed in contribution names, permalinks, authors, links, categories, or other metadata. They may be used in descriptions, posts, topics, comments, reports, and validation output.', 'CONTRIB_HIDDEN' => 'Hidden', 'CONTRIB_ISO_CODE' => 'ISO Code', 'CONTRIB_ISO_CODE_EXPLAIN' => 'The ISO code according to the Translation Coding Guidelines.', 'CONTRIB_LOCAL_NAME' => 'Local name', 'CONTRIB_LOCAL_NAME_EXPLAIN' => 'The localized name of the language, e.g. Français.', 'CONTRIB_NAME' => 'Contribution Name', - 'CONTRIB_NAME_EXISTS' => 'The unique name has already been reserved.', 'CONTRIB_NEW' => 'New', 'CONTRIB_NONACTIVE_AUTHORS' => 'Non-Active Co-Authors (Past Contributors)', 'CONTRIB_NONACTIVE_AUTHORS_EXPLAIN' => 'Non-Active Co-Authors can not manage anything for the contribution and are only listed as previous authors.', 'CONTRIB_NOT_FOUND' => 'The contribution you requested could not be found.', 'CONTRIB_OWNER_UPDATED' => 'The owner has been changed.', 'CONTRIB_PERMALINK' => 'Contribution Permalink', + 'CONTRIB_PERMALINK_EXISTS' => 'That contribution permalink is already in use.', 'CONTRIB_PERMALINK_EXPLAIN' => 'Cleaned version of the contribution name, used to build the url for the contribution.
Leave blank to have one automatically created based on the contribution name.', 'CONTRIB_RELEASE_DATE' => 'Release date', 'CONTRIB_STATUS' => 'Contribution status', @@ -118,7 +119,7 @@ 'EMPTY_CONTRIB_ISO_CODE' => 'Enter the ISO Code', 'EMPTY_CONTRIB_LOCAL_NAME' => 'Enter the local name', 'EMPTY_CONTRIB_NAME' => 'Enter the contrib name', - 'EMPTY_CONTRIB_PERMALINK' => 'Enter your proposal for permalink for the contribution', + 'EMPTY_CONTRIB_PERMALINK' => 'A valid contribution permalink could not be generated. Please enter one manually.', 'EMPTY_CONTRIB_TYPE' => 'Select at least one contribution type', 'ERROR_CONTRIB_EMAIL_FRIEND' => 'You are not permitted to recommend this contribution to someone else.', @@ -185,6 +186,7 @@ 'REVISION' => 'Revision', 'REVISIONS' => 'Revisions', 'REVISION_APPROVED' => 'Approved', + 'REVISION_EMOJI_NOT_ALLOWED' => 'Four-byte characters are not allowed in revision names, versions, licenses, or other revision metadata.', 'REVISION_DENIED' => 'Denied', 'REVISION_FOR_NEXT_PHPBB' => 'This revision has been submitted for the next phpBB release.', 'REVISION_IN_QUEUE' => 'You already have a revision in the validation queue. You must wait until the previous revision is approved or denied to submit a new one.', diff --git a/manage/tool/composer/rebuild_repo.php b/manage/tool/composer/rebuild_repo.php index d27f82d2f..b046e9c4e 100644 --- a/manage/tool/composer/rebuild_repo.php +++ b/manage/tool/composer/rebuild_repo.php @@ -19,6 +19,7 @@ use phpbb\titania\config\config as ext_config; use phpbb\titania\contribution\type\collection as type_collection; use phpbb\titania\controller\helper; +use phpbb\titania\unicode; use phpbb\titania\entity\package; use phpbb\titania\ext; use phpbb\titania\manage\tool\base; @@ -357,6 +358,7 @@ protected function get_composer_json($file) if ($path !== null) { $composer_json = file_get_contents($this->package->get_temp_path() . '/' . $path . '/composer.json'); + $composer_json = unicode::escape_json($composer_json); } $this->package->cleanup(); diff --git a/posting.php b/posting.php index 0525b4094..2c5f32988 100644 --- a/posting.php +++ b/posting.php @@ -809,10 +809,12 @@ public function split_topic($topic_id, $mode) // Use new subject as the first post's subject to avoid issues when it gets approved if (!$first_post['post_approved']) { - $sql = 'UPDATE ' . TITANIA_POSTS_TABLE . ' - SET post_subject = "' . $this->db->sql_escape($subject) . '" - WHERE post_id = ' . (int) $first_post['post_id']; - $this->db->sql_query($sql); + $first_post_object = new \titania_post($topic->topic_type, $topic, $first_post['post_id']); + if ($first_post_object->load()) + { + $first_post_object->post_subject = $subject; + $first_post_object->update(); + } } } } diff --git a/unicode.php b/unicode.php new file mode 100644 index 000000000..e984dfe26 --- /dev/null +++ b/unicode.php @@ -0,0 +1,83 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\titania; + +class unicode +{ + /** + * Escape four-byte UTF-8 characters as JSON surrogate pairs. + * + * This leaves the structure and formatting of the JSON document intact. + * + * @param string $json + * @return string + */ + public static function escape_json($json) + { + $escaped_json = preg_replace_callback( + '/[\x{10000}-\x{10FFFF}]/u', + function ($matches) + { + $codepoint = utf8_ord($matches[0]) - 0x10000; + $high_surrogate = 0xD800 + ($codepoint >> 10); + $low_surrogate = 0xDC00 + ($codepoint & 0x3FF); + + return sprintf('\\u%04X\\u%04X', $high_surrogate, $low_surrogate); + }, + $json + ); + + if ($escaped_json === null) + { + throw new \UnexpectedValueException('JSON contains invalid UTF-8.'); + } + + return $escaped_json; + } + + /** + * Check whether a string contains a four-byte character unsupported by + * phpBB's MySQL utf8 schema. + * + * @param string $value + * @param bool $decode_ncr Whether to decode numeric character references. + * @return bool + */ + public static function contains_unsupported($value, $decode_ncr = true) + { + if ($decode_ncr) + { + $value = utf8_decode_ncr($value); + } + + return (bool) preg_match('/[\x{10000}-\x{10FFFF}]/u', $value); + } + + /** + * Remove four-byte characters unsupported by phpBB's MySQL utf8 schema. + * + * @param string $value + * @return string + */ + public static function strip_unsupported($value) + { + $value = utf8_decode_ncr($value); + + return preg_replace( + '/[\x{10000}-\x{10FFFF}]/u', + ' ', + $value + ); + } +} diff --git a/url/url.php b/url/url.php index df8fbac38..55811a400 100644 --- a/url/url.php +++ b/url/url.php @@ -192,6 +192,7 @@ public function remove_nth_param($nth) */ public static function generate_slug($string) { + $string = \phpbb\titania\unicode::strip_unsupported($string); $string = self::url_replace($string, false); // Replace any number of spaces with a single underscore