diff --git a/.gitignore b/.gitignore index 102173e832..57ea182b2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,11 @@ -avatar/* -files/* -file/* -local/* -_darcs/* -logs/* -log/* -run/* +avatar/ +files/ +file/ +local/ +_darcs/ +logs/ +log/ +run/ config.php .htaccess httpd.conf diff --git a/CONFIGURE b/CONFIGURE index 92ae78204e..3fbc83e0bc 100644 --- a/CONFIGURE +++ b/CONFIGURE @@ -497,9 +497,9 @@ Profile management. biolimit: max character length of bio; 0 means no limit; null means to use the site text limit default. -backup: whether users can backup their own profiles. Defaults to true. +backup: whether users can backup their own profiles. Defaults to false. restore: whether users can restore their profiles from backup files. Defaults - to true. + to false. delete: whether users can delete their own accounts. Defaults to false. move: whether users can move their accounts to another server. Defaults to true. diff --git a/INSTALL b/INSTALL index ff755f2471..2ec73cb397 100644 --- a/INSTALL +++ b/INSTALL @@ -26,16 +26,12 @@ PHP modules The following software packages are *required* for this software to run correctly. -- PHP 5.5+ For newer versions, some functions that are used may be - disabled by default, such as the pcntl_* family. See the - section on 'Queues and daemons' for more information. -- MariaDB 5+ GNU Social uses, by default, a MariaDB server for data - storage. Versions 5.x and 10.x have both reportedly - worked well. It is also possible to run MySQL 5.5+. -- Web server Apache, lighttpd and nginx will all work. CGI mode is - recommended and also some variant of 'suexec' (or a - proper setup php-fpm pool) - NOTE: mod_rewrite or its equivalent is extremely useful. +- PHP 5.6+ PHP7.x is also supported. +- MariaDB 5+ MariaDB 10.x is also supported. +- Web server Apache, lighttpd and nginx will all work, see sample + configuration files in the web root. Please use PHP-FPM + and configure mod_rewrite (or equivalent) for an optimal + experience. Your PHP installation must include the following PHP extensions for a functional setup of GNU Social: @@ -49,22 +45,22 @@ functional setup of GNU Social: - php5-mysqlnd The native driver for PHP5 MariaDB connections. If you use MySQL, 'php5-mysql' or 'php5-mysqli' may be enough. -Or, for PHP7, some or all of these will be necessary. PHP7 support is still -experimental and not necessarily working: +Or, for PHP7, some or all of these will be necessary. PHP7 works and on +the development servers we are successful running PHP7.2. This is a good +list of PHP modules you will want installed with PHP7: php7.0-bcmath php7.0-curl php7.0-exif php7.0-gd php7.0-intl php7.0-mbstring - php7.0-mysqlnd + php7.0-mysql php7.0-opcache php7.0-readline php7.0-xmlwriter -The above package names are for Debian based systems. In the case of -Arch Linux, PHP is compiled with support for most extensions but they -require manual enabling in the relevant php.ini file (mostly php5-gmp). +NOTE: In Arch Linux, at least PHP5 requires manual enabling in the +relevant php.ini for some modules, most notably 'gmp'. Better performance ------------------ @@ -74,19 +70,10 @@ For some functionality, you will also need the following extensions: - opcache Improves performance a _lot_. Included in PHP, must be enabled manually in php.ini for most distributions. Find and set at least: opcache.enable=1 -- mailparse Efficient parsing of email requires this extension. - Submission by email or SMS-over-email uses this. -- sphinx A client for the sphinx server, an alternative to MySQL - or Postgresql fulltext search. You will also need a - Sphinx server to serve the search queries. - gettext For multiple languages. Default on many PHP installs; will be emulated if not present. - exif For thumbnails to be properly oriented. -You may also experience better performance from your site if you configure -a PHP cache/accelerator. Most distributions come with "opcache" support. -Enable it in your php.ini where it is documented together with its settings. - Installation ============ diff --git a/README.md b/README.md index 7dfe8ae05c..5822b366ff 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ So far it includes the following changes: - Backing up a user's account is more and more complete. - Emojis 😸 (utf8mb4 support) +- Fully qualified group mentions (!group@example.com) The last release, 1.1.3, gave us these improvements: diff --git a/actions/apistatusesupdate.php b/actions/apistatusesupdate.php index f2c70f5452..de00325494 100644 --- a/actions/apistatusesupdate.php +++ b/actions/apistatusesupdate.php @@ -46,7 +46,7 @@ /api/statuses/update.:format @par Formats (:format) - xml, json + xml, json, atom @par HTTP Method(s) POST @@ -339,6 +339,8 @@ class ApiStatusesUpdateAction extends ApiAuthAction $this->showSingleXmlStatus($this->notice); } elseif ($this->format == 'json') { $this->show_single_json_status($this->notice); + } elseif ($this->format == 'atom') { + $this->showSingleAtomStatus($this->notice); } } } diff --git a/actions/blockedfromgroup.php b/actions/blockedfromgroup.php index a2e7c5767f..d2873fe467 100644 --- a/actions/blockedfromgroup.php +++ b/actions/blockedfromgroup.php @@ -151,7 +151,7 @@ class GroupBlockList extends ProfileList $this->group = $group; } - function newListItem($profile) + function newListItem(Profile $profile) { return new GroupBlockListItem($profile, $this->group, $this->action); } diff --git a/actions/groupqueue.php b/actions/groupqueue.php index c50eff36f8..98da77e01a 100644 --- a/actions/groupqueue.php +++ b/actions/groupqueue.php @@ -153,7 +153,7 @@ class GroupqueueAction extends GroupAction // @todo FIXME: documentation missing. class GroupQueueList extends GroupMemberList { - function newListItem($profile) + function newListItem(Profile $profile) { return new GroupQueueListItem($profile, $this->group, $this->action); } diff --git a/actions/newnotice.php b/actions/newnotice.php index 5aa76a94e9..170e5bcdf8 100644 --- a/actions/newnotice.php +++ b/actions/newnotice.php @@ -47,6 +47,8 @@ class NewnoticeAction extends FormAction { protected $form = 'Notice'; + protected $inreplyto = null; + /** * Title of the page * @@ -75,6 +77,11 @@ class NewnoticeAction extends FormAction } } + if ($this->int('inreplyto')) { + // Throws exception if the inreplyto Notice is given but not found. + $this->inreplyto = Notice::getByID($this->int('inreplyto')); + } + // Backwards compatibility for "share this" widget things. // If no 'content', use 'status_textarea' $this->formOpts['content'] = $this->trimmed('content') ?: $this->trimmed('status_textarea'); @@ -132,13 +139,6 @@ class NewnoticeAction extends FormAction return; } - if ($this->int('inreplyto')) { - // Throws exception if the inreplyto Notice is given but not found. - $parent = Notice::getByID($this->int('inreplyto')); - } else { - $parent = null; - } - $act = new Activity(); $act->verb = ActivityVerb::POST; $act->time = time(); @@ -157,9 +157,9 @@ class NewnoticeAction extends FormAction $act->context = new ActivityContext(); - if ($parent instanceof Notice) { - $act->context->replyToID = $parent->getUri(); - $act->context->replyToUrl = $parent->getUrl(true); // maybe we don't have to send true here to force a URL? + if ($this->inreplyto instanceof Notice) { + $act->context->replyToID = $this->inreplyto->getUri(); + $act->context->replyToUrl = $this->inreplyto->getUrl(true); // maybe we don't have to send true here to force a URL? } if ($this->scoped->shareLocation()) { @@ -188,14 +188,14 @@ class NewnoticeAction extends FormAction // FIXME: We should be able to get the attentions from common_render_content! // and maybe even directly save whether they're local or not! - $act->context->attention = common_get_attentions($content, $this->scoped, $parent); + $act->context->attention = common_get_attentions($content, $this->scoped, $this->inreplyto); // $options gets filled with possible scoping settings ToSelector::fillActivity($this, $act, $options); $actobj = new ActivityObject(); $actobj->type = ActivityObject::NOTE; - $actobj->content = common_render_content($content, $this->scoped, $parent); + $actobj->content = common_render_content($content, $this->scoped, $this->inreplyto); // Finally add the activity object to our activity $act->objects[] = $actobj; @@ -224,6 +224,9 @@ class NewnoticeAction extends FormAction if ($this->getInfo() && $this->stored instanceof Notice) { $this->showNotice($this->stored); } elseif (!$this->getError()) { + if (!GNUsocial::isAjax() && $this->inreplyto instanceof Notice) { + $this->showNotice($this->inreplyto); + } parent::showContent(); } } diff --git a/actions/noticesearch.php b/actions/noticesearch.php index 2886700f6a..0d6fb51fb4 100644 --- a/actions/noticesearch.php +++ b/actions/noticesearch.php @@ -185,7 +185,7 @@ class SearchNoticeList extends NoticeList { $this->terms = $terms; } - function newListItem($notice) + function newListItem(Notice $notice) { return new SearchNoticeListItem($notice, $this->out, $this->terms); } diff --git a/actions/peopletagged.php b/actions/peopletagged.php index 1b0f897c11..db2420a8a3 100644 --- a/actions/peopletagged.php +++ b/actions/peopletagged.php @@ -167,7 +167,7 @@ class PeopletagMemberList extends ProfileList $this->peopletag = $peopletag; } - function newListItem($profile) + function newListItem(Profile $profile) { return new PeopletagMemberListItem($profile, $this->peopletag, $this->action); } diff --git a/actions/peopletagsubscribers.php b/actions/peopletagsubscribers.php index e5be8a3ff4..927cf66e64 100644 --- a/actions/peopletagsubscribers.php +++ b/actions/peopletagsubscribers.php @@ -167,7 +167,7 @@ class PeopletagSubscriberList extends ProfileList $this->peopletag = $peopletag; } - function newListItem($profile) + function newListItem(Profile $profile) { return new PeopletagSubscriberListItem($profile, $this->peopletag, $this->action); } diff --git a/actions/showstream.php b/actions/showstream.php index 1e70ecd3ac..79a3706d77 100644 --- a/actions/showstream.php +++ b/actions/showstream.php @@ -113,6 +113,18 @@ class ShowstreamAction extends NoticestreamAction $this->target->getNickname(), $this->tag))); } + if (!$this->target->isLocal()) { + // remote profiles at least have Atom, but we can't guarantee anything else + return array( + new Feed(Feed::ATOM, + $this->target->getAtomFeed(), + // TRANS: Title for link to notice feed. + // TRANS: %s is a user nickname. + sprintf(_('Notice feed for %s (Atom)'), + $this->target->getNickname())) + ); + } + return array(new Feed(Feed::JSON, common_local_url('ApiTimelineUser', array( @@ -139,10 +151,7 @@ class ShowstreamAction extends NoticestreamAction sprintf(_('Notice feed for %s (RSS 2.0)'), $this->target->getNickname())), new Feed(Feed::ATOM, - common_local_url('ApiTimelineUser', - array( - 'id' => $this->target->getID(), - 'format' => 'atom')), + $this->target->getAtomFeed(), // TRANS: Title for link to notice feed. // TRANS: %s is a user nickname. sprintf(_('Notice feed for %s (Atom)'), @@ -221,13 +230,14 @@ class ShowstreamAction extends NoticestreamAction $this->showEmptyListMessage(); } - $args = array('nickname' => $this->target->getNickname()); + // either nickname or id will be used, depending on which action (showstream, userbyid...) + $args = array('nickname' => $this->target->getNickname(), 'id' => $this->target->getID()); if (!empty($this->tag)) { $args['tag'] = $this->tag; } $this->pagination($this->page>1, $cnt>NOTICES_PER_PAGE, $this->page, - 'showstream', $args); + $this->getActionName(), $args); } function showAnonymousMessage() diff --git a/classes/Conversation.php b/classes/Conversation.php index 1dba2c1f4a..d18321deba 100644 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -36,6 +36,7 @@ class Conversation extends Managed_DataObject public $__table = 'conversation'; // table name public $id; // int(4) primary_key not_null auto_increment public $uri; // varchar(191) unique_key not 255 because utf8mb4 takes more space + public $url; // varchar(191) unique_key not 255 because utf8mb4 takes more space public $created; // datetime not_null public $modified; // timestamp not_null default_CURRENT_TIMESTAMP @@ -45,6 +46,7 @@ class Conversation extends Managed_DataObject 'fields' => array( 'id' => array('type' => 'serial', 'not null' => true, 'description' => 'Unique identifier, (again) unrelated to notice id since 2016-01-06'), 'uri' => array('type' => 'varchar', 'not null'=>true, 'length' => 191, 'description' => 'URI of the conversation'), + 'url' => array('type' => 'varchar', 'length' => 191, 'description' => 'Resolvable URL, preferrably remote (local can be generated on the fly)'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), ), @@ -89,15 +91,21 @@ class Conversation extends Managed_DataObject * * @return Conversation the new conversation DO */ - static function create($uri=null, $created=null) + static function create(ActivityContext $ctx=null, $created=null) { // Be aware that the Notice does not have an id yet since it's not inserted! $conv = new Conversation(); $conv->created = $created ?: common_sql_now(); - $conv->uri = $uri ?: sprintf('%s%s=%s:%s=%s', + if ($ctx instanceof ActivityContext) { + $conv->uri = $ctx->conversation; + $conv->url = $ctx->conversation_url; + } else { + $conv->uri = sprintf('%s%s=%s:%s=%s', TagURI::mint(), 'objectType', 'thread', 'nonce', common_random_hexstr(8)); + $conv->url = null; // locally generated Conversation objects don't get static URLs stored + } // This insert throws exceptions on failure $conv->insert(); diff --git a/classes/File.php b/classes/File.php index b0da3f09f3..67b87efd0d 100644 --- a/classes/File.php +++ b/classes/File.php @@ -194,10 +194,14 @@ class File extends Managed_DataObject } $redir = File_redirection::where($given_url); - $file = $redir->getFile(); - - if (!$file instanceof File || empty($file->id)) { + try { + $file = $redir->getFile(); + } catch (EmptyPkeyValueException $e) { + common_log(LOG_ERR, 'File_redirection::where gave object with empty file_id for given_url '._ve($given_url)); + throw new ServerException('URL processing failed without new File object'); + } catch (NoResultException $e) { // This should not happen + common_log(LOG_ERR, 'File_redirection after discovery could still not return a File object.'); throw new ServerException('URL processing failed without new File object'); } @@ -731,16 +735,18 @@ class File extends Managed_DataObject $dupfile = new File(); // First we find file entries that would be duplicates of this when shortened // ... and we'll just throw the dupes out the window for now! It's already so borken. - $dupfile->query(sprintf('SELECT * FROM file WHERE LEFT(url, 191) = "%1$s"', $file->shortenedurl)); + $dupfile->query(sprintf('SELECT * FROM file WHERE LEFT(url, 191) = %1$s', $dupfile->_quote($file->shortenedurl))); // Leave one of the URLs in the database by using ->find(true) (fetches first entry) if ($dupfile->find(true)) { print "\nShortening url entry for $table id: {$file->id} ["; $orig = clone($dupfile); + $origurl = $dupfile->url; // save for logging purposes $dupfile->url = $file->shortenedurl; // make sure it's only 191 chars from now on $dupfile->update($orig); print "\nDeleting duplicate entries of too long URL on $table id: {$file->id} ["; // only start deleting with this fetch. while($dupfile->fetch()) { + common_log(LOG_INFO, sprintf('Deleting duplicate File entry of %1$d: %2$d (original URL: %3$s collides with these first 191 characters: %4$s', $dupfile->id, $file->id, $origurl, $file->shortenedurl)); print "."; $dupfile->delete(); } diff --git a/classes/File_redirection.php b/classes/File_redirection.php index d1b266c90b..742a6143cc 100644 --- a/classes/File_redirection.php +++ b/classes/File_redirection.php @@ -445,8 +445,8 @@ class File_redirection extends Managed_DataObject } public function getFile() { - if(empty($this->file) && $this->file_id) { - $this->file = File::getKV('id', $this->file_id); + if (!$this->file instanceof File) { + $this->file = File::getByID($this->file_id); } return $this->file; diff --git a/classes/Foreign_link.php b/classes/Foreign_link.php index b3757448ad..8388f12e72 100644 --- a/classes/Foreign_link.php +++ b/classes/Foreign_link.php @@ -89,7 +89,7 @@ class Foreign_link extends Managed_DataObject return $flink; } - function set_flags($noticesend, $noticerecv, $replysync, $friendsync) + function set_flags($noticesend, $noticerecv, $replysync, $repeatsync, $friendsync) { if ($noticesend) { $this->noticesync |= FOREIGN_NOTICE_SEND; @@ -109,6 +109,12 @@ class Foreign_link extends Managed_DataObject $this->noticesync &= ~FOREIGN_NOTICE_SEND_REPLY; } + if ($repeatsync) { + $this->noticesync |= FOREIGN_NOTICE_SEND_REPEAT; + } else { + $this->noticesync &= ~FOREIGN_NOTICE_SEND_REPEAT; + } + if ($friendsync) { $this->friendsync |= FOREIGN_FRIEND_RECV; } else { diff --git a/classes/Notice.php b/classes/Notice.php index d5a0e5f6d2..15b96ae670 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -320,6 +320,21 @@ class Notice extends Managed_DataObject } } + public function getSelfLink() + { + if ($this->isLocal()) { + return common_local_url('ApiStatusesShow', array('id' => $this->getID(), 'format' => 'atom')); + } + + $selfLink = $this->getPref('ostatus', 'self'); + + if (!common_valid_http_url($selfLink)) { + throw new InvalidUrlException($selfLink); + } + + return $selfLink; + } + public function getObjectType($canonical=false) { if (is_null($this->object_type) || $this->object_type==='') { throw new NoObjectTypeException($this); @@ -442,6 +457,7 @@ class Notice extends Managed_DataObject static function saveNew($profile_id, $content, $source, array $options=null) { $defaults = array('uri' => null, 'url' => null, + 'self' => null, 'conversation' => null, // URI of conversation 'reply_to' => null, // This will override convo URI if the parent is known 'repeat_of' => null, // This will override convo URI if the repeated notice is known @@ -624,8 +640,13 @@ class Notice extends Managed_DataObject } else { // Conversation entry with specified URI was not found, so we must create it. common_debug('Conversation URI not found, so we will create it with the URI given in the options to Notice::saveNew: '.$options['conversation']); + $convctx = new ActivityContext(); + $convctx->conversation = $options['conversation']; + if (array_key_exists('conversation_url', $options)) { + $convctx->conversation_url = $options['conversation_url']; + } // The insert in Conversation::create throws exception on failure - $conv = Conversation::create($options['conversation'], $notice->created); + $conv = Conversation::create($convctx, $notice->created); } $notice->conversation = $conv->getID(); unset($conv); @@ -706,6 +727,10 @@ class Notice extends Managed_DataObject } } + if ($self && common_valid_http_url($self)) { + $notice->setPref('ostatus', 'self', $self); + } + // Only save 'attention' and metadata stuff (URLs, tags...) stuff if // the activityverb is a POST (since stuff like repeat, favorite etc. // reasonably handle notifications themselves. @@ -765,6 +790,9 @@ class Notice extends Managed_DataObject // implied object $options['uri'] = $act->id; $options['url'] = $act->link; + if ($act->selfLink) { + $options['self'] = $act->selfLink; + } } else { $actobj = count($act->objects)===1 ? $act->objects[0] : null; if (!is_null($actobj) && !empty($actobj->id)) { @@ -775,6 +803,9 @@ class Notice extends Managed_DataObject $options['url'] = $actobj->id; } } + if ($actobj->selfLink) { + $options['self'] = $actobj->selfLink; + } } $defaults = array( @@ -784,6 +815,7 @@ class Notice extends Managed_DataObject 'reply_to' => null, 'repeat_of' => null, 'scope' => null, + 'self' => null, 'source' => 'unknown', 'tags' => array(), 'uri' => null, @@ -921,7 +953,7 @@ class Notice extends Managed_DataObject // Conversation entry with specified URI was not found, so we must create it. common_debug('Conversation URI not found, so we will create it with the URI given in the context of the activity: '.$act->context->conversation); // The insert in Conversation::create throws exception on failure - $conv = Conversation::create($act->context->conversation, $stored->created); + $conv = Conversation::create($act->context, $stored->created); } $stored->conversation = $conv->getID(); unset($conv); @@ -1020,6 +1052,14 @@ class Notice extends Managed_DataObject throw new ServerException('Supposedly saved Notice has no ID.'); } + if ($self && common_valid_http_url($self)) { + $stored->setPref('ostatus', 'self', $self); + } + + if ($self && common_valid_http_url($self)) { + $stored->setPref('ostatus', 'self', $self); + } + // Only save 'attention' and metadata stuff (URLs, tags...) stuff if // the activityverb is a POST (since stuff like repeat, favorite etc. // reasonably handle notifications themselves. @@ -1578,12 +1618,12 @@ class Notice extends Managed_DataObject if (common_config('group', 'addtag')) { // we automatically add a tag for every group name, too - - $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->nickname), - 'notice_id' => $this->id)); + common_debug('Adding hashtag matching group nickname: '._ve($group->getNickname())); + $tag = Notice_tag::pkeyGet(array('tag' => common_canonical_tag($group->getNickname()), + 'notice_id' => $this->getID())); if (is_null($tag)) { - $this->saveTag($group->nickname); + $this->saveTag($group->getNickname()); } } @@ -2008,6 +2048,7 @@ class Notice extends Managed_DataObject $conv = Conversation::getKV('id', $this->conversation); if ($conv instanceof Conversation) { $ctx->conversation = $conv->uri; + $ctx->conversation_url = $conv->url; } } @@ -2070,9 +2111,12 @@ class Notice extends Managed_DataObject } } + try { + $act->selfLink = $this->getSelfLink(); + } catch (InvalidUrlException $e) { + $act->selfLink = null; + } if ($this->isLocal()) { - $act->selfLink = common_local_url('ApiStatusesShow', array('id' => $this->id, - 'format' => 'atom')); $act->editLink = $act->selfLink; } @@ -2170,8 +2214,13 @@ class Notice extends Managed_DataObject $object->title = sprintf('New %1$s by %2$s', ActivityObject::canonicalType($object->type), $this->getProfile()->getNickname()); $object->content = $this->getRendered(); $object->link = $this->getUrl(); + try { + $object->selfLink = $this->getSelfLink(); + } catch (InvalidUrlException $e) { + $object->selfLink = null; + } - $object->extra[] = array('status_net', array('notice_id' => $this->id)); + $object->extra[] = array('statusnet:notice_id', null, $this->id); Event::handle('EndActivityObjectFromNotice', array($this, &$object)); } @@ -3195,4 +3244,27 @@ class Notice extends Managed_DataObject } } } + + public function delPref($namespace, $topic) { + return Notice_prefs::setData($this, $namespace, $topic, null); + } + + public function getPref($namespace, $topic, $default=null) { + // If you want an exception to be thrown, call Notice_prefs::getData directly + try { + return Notice_prefs::getData($this, $namespace, $topic, $default); + } catch (NoResultException $e) { + return null; + } + } + + // The same as getPref but will fall back to common_config value for the same namespace/topic + public function getConfigPref($namespace, $topic) + { + return Notice_prefs::getConfigData($this, $namespace, $topic); + } + + public function setPref($namespace, $topic, $data) { + return Notice_prefs::setData($this, $namespace, $topic, $data); + } } diff --git a/classes/Notice_prefs.php b/classes/Notice_prefs.php new file mode 100644 index 0000000000..1bbf309da4 --- /dev/null +++ b/classes/Notice_prefs.php @@ -0,0 +1,172 @@ +. + * + * @category Data + * @package GNUsocial + * @author Mikael Nordfeldth + * @copyright 2013 Free Software Foundation, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +class Notice_prefs extends Managed_DataObject +{ + public $__table = 'notice_prefs'; // table name + public $notice_id; // int(4) primary_key not_null + public $namespace; // varchar(191) not_null + public $topic; // varchar(191) not_null + public $data; // text + public $created; // datetime not_null default_0000-00-00%2000%3A00%3A00 + public $modified; // timestamp not_null default_CURRENT_TIMESTAMP + + public static function schemaDef() + { + return array( + 'fields' => array( + 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'user'), + 'namespace' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'namespace, like pluginname or category'), + 'topic' => array('type' => 'varchar', 'length' => 191, 'not null' => true, 'description' => 'preference key, i.e. description, age...'), + 'data' => array('type' => 'blob', 'description' => 'topic data, may be anything'), + 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), + 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'), + ), + 'primary key' => array('notice_id', 'namespace', 'topic'), + 'foreign keys' => array( + 'notice_prefs_notice_id_fkey' => array('notice', array('notice_id' => 'id')), + ), + 'indexes' => array( + 'notice_prefs_notice_id_idx' => array('notice_id'), + ), + ); + } + + static function getNamespacePrefs(Notice $notice, $namespace, array $topic=array()) + { + if (empty($topic)) { + $prefs = new Notice_prefs(); + $prefs->notice_id = $notice->getID(); + $prefs->namespace = $namespace; + $prefs->find(); + } else { + $prefs = self::pivotGet('notice_id', $notice->getID(), array('namespace'=>$namespace, 'topic'=>$topic)); + } + + if (empty($prefs->N)) { + throw new NoResultException($prefs); + } + + return $prefs; + } + + static function getNamespace(Notice $notice, $namespace, array $topic=array()) + { + $prefs = self::getNamespacePrefs($notice, $namespace, $topic); + return $prefs->fetchAll(); + } + + static function getAll(Notice $notice) + { + try { + $prefs = self::listFind('notice_id', array($notice->getID())); + } catch (NoResultException $e) { + return array(); + } + + $list = array(); + while ($prefs->fetch()) { + if (!isset($list[$prefs->namespace])) { + $list[$prefs->namespace] = array(); + } + $list[$prefs->namespace][$prefs->topic] = $prefs->data; + } + return $list; + } + + static function getTopic(Notice $notice, $namespace, $topic) { + return self::getByPK(array('notice_id' => $notice->getID(), + 'namespace' => $namespace, + 'topic' => $topic)); + } + + static function getData(Notice $notice, $namespace, $topic, $def=null) { + try { + $pref = self::getTopic($notice, $namespace, $topic); + } catch (NoResultException $e) { + if ($def === null) { + // If no default value was set, continue the exception. + throw $e; + } + // If there was a default value, return that. + return $def; + } + return $pref->data; + } + + static function getConfigData(Notice $notice, $namespace, $topic) { + try { + $data = self::getData($notice, $namespace, $topic); + } catch (NoResultException $e) { + $data = common_config($namespace, $topic); + } + return $data; + } + + /* + * Sets a notice preference based on Notice, namespace and topic + * + * @param Notice $notice Which notice this is for + * @param string $namespace Under which namespace (pluginname etc.) + * @param string $topic Preference name (think key in key-val store) + * @param string $data Data to be put into preference storage, null means delete + * + * @return true if changes are made, false if no action taken + * @throws ServerException if preference could not be saved + */ + static function setData(Notice $notice, $namespace, $topic, $data=null) { + try { + $pref = self::getTopic($notice, $namespace, $topic); + if (is_null($data)) { + $pref->delete(); + } else { + $orig = clone($pref); + $pref->data = $data; + $pref->update($orig); + } + return true; + } catch (NoResultException $e) { + if (is_null($data)) { + return false; // No action taken + } + } + + $pref = new Notice_prefs(); + $pref->notice_id = $notice->getID(); + $pref->namespace = $namespace; + $pref->topic = $topic; + $pref->data = $data; + $pref->created = common_sql_now(); + + if ($pref->insert() === false) { + throw new ServerException('Could not save notice preference.'); + } + return true; + } +} diff --git a/classes/Profile.php b/classes/Profile.php index fb6a621273..c2cc9b26e2 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -89,9 +89,14 @@ class Profile extends Managed_DataObject public function getUser() { if (!isset($this->_user[$this->id])) { - $user = User::getKV('id', $this->id); - if (!$user instanceof User) { - throw new NoSuchUserException(array('id'=>$this->id)); + $cur_user = common_current_user(); + if (($cur_user instanceof User) && $cur_user->sameAs($this)) { + $user = $cur_user; + } else { + $user = User::getKV('id', $this->id); + if (!$user instanceof User) { + throw new NoSuchUserException(array('id'=>$this->id)); + } } $this->_user[$this->id] = $user; } @@ -941,11 +946,6 @@ class Profile extends Managed_DataObject function delete($useWhere=false) { - // just in case it hadn't been done before... (usually set before adding deluser to queue handling!) - if (!$this->hasRole(Profile_role::DELETED)) { - $this->grantRole(Profile_role::DELETED); - } - $this->_deleteNotices(); $this->_deleteSubscriptions(); $this->_deleteTags(); @@ -957,6 +957,7 @@ class Profile extends Managed_DataObject // not on individual objects. $related = array('Reply', 'Group_member', + 'Profile_role' ); Event::handle('ProfileDeleteRelated', array($this, &$related)); @@ -965,6 +966,8 @@ class Profile extends Managed_DataObject $inst->profile_id = $this->id; $inst->delete(); } + + $this->grantRole(Profile_role::DELETED); $localuser = User::getKV('id', $this->id); if ($localuser instanceof User) { @@ -1532,6 +1535,14 @@ class Profile extends Managed_DataObject } return $url; } + public function getHtmlTitle() + { + try { + return $this->getAcctUri(false); + } catch (ProfileNoAcctUriException $e) { + return $this->getNickname(); + } + } public function getNickname() { @@ -1612,14 +1623,13 @@ class Profile extends Managed_DataObject return !empty($block); } - function getAtomFeed() + public function getAtomFeed() { $feed = null; if (Event::handle('StartProfileGetAtomFeed', array($this, &$feed))) { - $user = User::getKV('id', $this->id); - if (!empty($user)) { - $feed = common_local_url('ApiTimelineUser', array('id' => $user->id, + if ($this->isLocal()) { + $feed = common_local_url('ApiTimelineUser', array('id' => $this->getID(), 'format' => 'atom')); } Event::handle('EndProfileGetAtomFeed', array($this, $feed)); diff --git a/classes/User.php b/classes/User.php index 952b74134b..b4f263235b 100644 --- a/classes/User.php +++ b/classes/User.php @@ -289,6 +289,11 @@ class User extends Managed_DataObject // TRANS: Profile data could not be inserted for some reason. throw new ServerException(_m('Could not insert profile data for new user.')); } + + // Necessary because id has been known to be reissued. + if ($profile->hasRole(Profile_role::DELETED)) { + $profile->revokeRole(Profile_role::DELETED); + } $user->id = $id; diff --git a/db/core.php b/db/core.php index f654d79d99..56213eb338 100644 --- a/db/core.php +++ b/db/core.php @@ -41,6 +41,7 @@ $classes = array('Schema_version', 'Notice', 'Notice_location', 'Notice_source', + 'Notice_prefs', 'Reply', 'Consumer', 'Token', diff --git a/doc-src/twitterapi b/doc-src/twitterapi index 97f4c2ae95..69303a9628 100644 --- a/doc-src/twitterapi +++ b/doc-src/twitterapi @@ -14,7 +14,7 @@ check out [Beginner’s Guide to OAuth](http://hueniverse.com/oauth/)). To use OAuth, you'll need to register your client application via the web interface and obtain a consumer key and secret. You can find the interface for application -registration at [http://%%site.server%%/%%site.path%%settings/oauthapps](http://%%site.server%%/%%site.path%%settings/oauthapps). +registration at [%%action.oauthappssettings%%](%%action.oauthappssettings%%). ## JSONP callbacks diff --git a/extlib/Auth/OpenID/Parse.php b/extlib/Auth/OpenID/Parse.php index 0461bdcff7..af0b86af7e 100644 --- a/extlib/Auth/OpenID/Parse.php +++ b/extlib/Auth/OpenID/Parse.php @@ -218,21 +218,11 @@ class Auth_OpenID_Parse { function match($regexp, $text, &$match) { - if (!is_callable('mb_ereg_search_init')) { - if (!preg_match($regexp, $text, $match)) { - return false; - } - $match = $match[0]; - return true; + if (preg_match($regexp, $text, $match)) { + return true; } - $regexp = substr($regexp, 1, strlen($regexp) - 2 - strlen($this->_re_flags)); - mb_ereg_search_init($text); - if (!mb_ereg_search($regexp)) { - return false; - } - $match = mb_ereg_search_getregs(); - return true; + return false; } /** diff --git a/extlib/DB.php b/extlib/DB.php index f36f919fe9..1aa9477065 100644 --- a/extlib/DB.php +++ b/extlib/DB.php @@ -426,7 +426,7 @@ define('DB_PORTABILITY_ALL', 63); * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB @@ -577,7 +577,7 @@ class DB */ function apiVersion() { - return '1.8.2'; + return '1.9.2'; } // }}} @@ -772,7 +772,7 @@ class DB $parsed['dbsyntax'] = $str; } - if (!count($dsn)) { + if (!strlen($dsn)) { return $parsed; } @@ -941,7 +941,7 @@ class DB * @author Stig Bakken * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_Error extends PEAR_Error @@ -959,18 +959,32 @@ class DB_Error extends PEAR_Error * * @see PEAR_Error */ - function DB_Error($code = DB_ERROR, $mode = PEAR_ERROR_RETURN, + function __construct($code = DB_ERROR, $mode = PEAR_ERROR_RETURN, $level = E_USER_NOTICE, $debuginfo = null) { if (is_int($code)) { - $this->PEAR_Error('DB Error: ' . DB::errorMessage($code), $code, - $mode, $level, $debuginfo, common_log(LOG_ERR, var_export($debuginfo,true))); + parent::__construct('DB Error: ' . DB::errorMessage($code), $code, + $mode, $level, $debuginfo); } else { - $this->PEAR_Error("DB Error: $code", DB_ERROR, + parent::__construct("DB Error: $code", DB_ERROR, $mode, $level, $debuginfo); } } + /** + * Workaround to both avoid the "Redefining already defined constructor" + * PHP error and provide backward compatibility in case someone is calling + * DB_Error() dynamically + */ + public function __call($method, $arguments) + { + if ($method == 'DB_Error') { + return call_user_func_array(array($this, '__construct'), $arguments); + } + trigger_error( + 'Call to undefined method DB_Error::' . $method . '()', E_USER_ERROR + ); + } // }}} } @@ -988,7 +1002,7 @@ class DB_Error extends PEAR_Error * @author Stig Bakken * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_result @@ -1095,7 +1109,7 @@ class DB_result * * @return void */ - function DB_result(&$dbh, $result, $options = array()) + function __construct(&$dbh, $result, $options = array()) { $this->autofree = $dbh->options['autofree']; $this->dbh = &$dbh; @@ -1453,7 +1467,7 @@ class DB_result * @author Stig Bakken * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB * @see DB_common::setFetchMode() */ @@ -1468,7 +1482,7 @@ class DB_row * * @return void */ - function DB_row(&$arr) + function __construct(&$arr) { foreach ($arr as $key => $value) { $this->$key = &$arr[$key]; diff --git a/extlib/DB/DataObject.php b/extlib/DB/DataObject.php index 1a7b34665d..e26cf8efa0 100644 --- a/extlib/DB/DataObject.php +++ b/extlib/DB/DataObject.php @@ -15,7 +15,7 @@ * @author Alan Knowles * @copyright 1997-2006 The PHP Group * @license http://www.php.net/license/3_01.txt PHP License 3.01 - * @version CVS: $Id: DataObject.php 320069 2011-11-28 04:34:08Z alan_k $ + * @version CVS: $Id: DataObject.php 336751 2015-05-12 04:39:50Z alan_k $ * @link http://pear.php.net/package/DB_DataObject */ @@ -410,7 +410,7 @@ class DB_DataObject extends DB_DataObject_Overload if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { $this->debug($n, "find",1); } - if (!$this->__table) { + if (!strlen($this->tableName())) { // xdebug can backtrace this! trigger_error("NO \$__table SPECIFIED in class definition",E_USER_ERROR); } @@ -2073,6 +2073,9 @@ class DB_DataObject extends DB_DataObject_Overload if (count($args)) { $this->__table = $args[0]; } + if (empty($this->__table)) { + return ''; + } if (!empty($_DB_DATAOBJECT['CONFIG']['portability']) && $_DB_DATAOBJECT['CONFIG']['portability'] & 1) { return strtolower($this->__table); } @@ -2421,7 +2424,7 @@ class DB_DataObject extends DB_DataObject_Overload $dsn = isset($this->_database_dsn) ? $this->_database_dsn : null; if (!$dsn) { - if (!$this->_database && !empty($this->__table)) { + if (!$this->_database && !strlen($this->tableName())) { $this->_database = isset($options["table_{$this->tableName()}"]) ? $options["table_{$this->tableName()}"] : null; } if (!empty($_DB_DATAOBJECT['CONFIG']['debug'])) { @@ -3522,7 +3525,7 @@ class DB_DataObject extends DB_DataObject_Overload if ($joinCol !== false) { $this->raiseError( "joinAdd: You cannot target a join column in the " . - "'link from' table ({$obj->__table}). " . + "'link from' table ({$obj->tableName()}). " . "Either remove the fourth argument to joinAdd() ". "({$joinCol}), or alter your links.ini file.", DB_DATAOBJECT_ERROR_NODATA); @@ -3605,7 +3608,7 @@ class DB_DataObject extends DB_DataObject_Overload if (!$items) { $this->raiseError( - "joinAdd: No table definition for {$obj->__table}", + "joinAdd: No table definition for {$obj->tableName()}", DB_DATAOBJECT_ERROR_INVALIDCONFIG); return false; } @@ -3800,6 +3803,7 @@ class DB_DataObject extends DB_DataObject_Overload */ function autoJoin($cfg = array()) { + global $_DB_DATAOBJECT; //var_Dump($cfg);exit; $pre_links = $this->links(); if (!empty($cfg['links'])) { @@ -3807,7 +3811,8 @@ class DB_DataObject extends DB_DataObject_Overload } $map = $this->links( ); - + $this->databaseStructure(); + $dbstructure = $_DB_DATAOBJECT['INI'][$this->_database]; //print_r($map); $tabdef = $this->table(); @@ -3874,6 +3879,12 @@ class DB_DataObject extends DB_DataObject_Overload list($tab,$col) = explode(':', $info); // what about multiple joins on the same table!!! + + // if links point to a table that does not exist - ignore. + if (!isset($dbstructure[$tab])) { + continue; + } + $xx = DB_DataObject::factory($tab); if (!is_object($xx) || !is_a($xx, 'DB_DataObject')) { continue; diff --git a/extlib/DB/DataObject/Generator.php b/extlib/DB/DataObject/Generator.php index a712e6d9eb..c7f87161c3 100644 --- a/extlib/DB/DataObject/Generator.php +++ b/extlib/DB/DataObject/Generator.php @@ -15,7 +15,7 @@ * @author Alan Knowles * @copyright 1997-2006 The PHP Group * @license http://www.php.net/license/3_01.txt PHP License 3.01 - * @version CVS: $Id: Generator.php 315531 2011-08-26 02:21:29Z alan_k $ + * @version CVS: $Id: Generator.php 336719 2015-05-05 10:37:33Z alan_k $ * @link http://pear.php.net/package/DB_DataObject */ @@ -406,7 +406,7 @@ class DB_DataObject_Generator extends DB_DataObject * Currenly only works with mysql / mysqli / posgtreas * to use, you must set option: generate_links=true * - * @author Pascal Schni + * @author Pascal Sch�ni */ function _createForiegnKeys() @@ -507,7 +507,7 @@ class DB_DataObject_Generator extends DB_DataObject * Currenly only works with mysql / mysqli * to use, you must set option: generate_links=true * - * @author Pascal Schni + * @author Pascal Sch�ni */ function generateForeignKeys() { @@ -895,7 +895,7 @@ class DB_DataObject_Generator extends DB_DataObject $options = &PEAR::getStaticProperty('DB_DataObject','options'); $this->_extends = empty($options['extends']) ? $this->_extends : $options['extends']; - $this->_extendsFile = empty($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; + $this->_extendsFile = !isset($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; foreach($this->tables as $this->table) { @@ -976,8 +976,12 @@ class DB_DataObject_Generator extends DB_DataObject $head .= $this->derivedHookExtendsDocBlock(); - // requires - $head .= "require_once '{$this->_extendsFile}';\n\n"; + // requires - if you set extends_location = (blank) then no require line will be set + // this can be used if you have an autoloader + + if (!empty($this->_extendsFile)) { + $head .= "require_once '{$this->_extendsFile}';\n\n"; + } // add dummy class header in... // class $head .= $this->derivedHookClassDocBlock(); @@ -1039,10 +1043,11 @@ class DB_DataObject_Generator extends DB_DataObject continue; } - $p = str_repeat(' ',max(2, (30 - strlen($t->name)))); + $pad = str_repeat(' ',max(2, (30 - strlen($t->name)))); $length = empty($t->len) ? '' : '('.$t->len.')'; - $body .=" {$var} \${$t->name}; {$p}// {$t->type}$length {$t->flags}\n"; + $flags = strlen($t->flags) ? (' '. trim($t->flags)) : ''; + $body .=" {$var} \${$t->name}; {$pad}// {$t->type}{$length}{$flags}\n"; // can not do set as PEAR::DB table info doesnt support it. //if (substr($t->Type,0,3) == "set") @@ -1283,7 +1288,7 @@ class DB_DataObject_Generator extends DB_DataObject $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix']; $this->_extends = empty($options['extends']) ? $this->_extends : $options['extends']; - $this->_extendsFile = empty($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; + $this->_extendsFile = !isset($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; $classname = $this->classname = $this->getClassNameFromTableName($this->table); diff --git a/extlib/DB/common.php b/extlib/DB/common.php index 27829a072a..73e3eb69f7 100644 --- a/extlib/DB/common.php +++ b/extlib/DB/common.php @@ -42,7 +42,7 @@ require_once 'PEAR.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_common extends PEAR @@ -145,7 +145,7 @@ class DB_common extends PEAR * * @return void */ - function DB_common() + function __construct() { $this->PEAR('DB_Error'); } diff --git a/extlib/DB/dbase.php b/extlib/DB/dbase.php index df36f972e3..17750cd5d2 100644 --- a/extlib/DB/dbase.php +++ b/extlib/DB/dbase.php @@ -41,7 +41,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_dbase extends DB_common @@ -140,13 +140,13 @@ class DB_dbase extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_dbase() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/fbsql.php b/extlib/DB/fbsql.php index b719da38e2..c32a08d120 100644 --- a/extlib/DB/fbsql.php +++ b/extlib/DB/fbsql.php @@ -41,7 +41,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB * @since Class functional since Release 1.7.0 */ @@ -124,13 +124,13 @@ class DB_fbsql extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_fbsql() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/ibase.php b/extlib/DB/ibase.php index 209ac6c3a1..60e07b5fc3 100644 --- a/extlib/DB/ibase.php +++ b/extlib/DB/ibase.php @@ -49,7 +49,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB * @since Class became stable in Release 1.7.0 */ @@ -180,13 +180,13 @@ class DB_ibase extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_ibase() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/ifx.php b/extlib/DB/ifx.php index e3150f92fb..5c5709f79e 100644 --- a/extlib/DB/ifx.php +++ b/extlib/DB/ifx.php @@ -48,7 +48,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_ifx extends DB_common @@ -167,13 +167,13 @@ class DB_ifx extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_ifx() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/msql.php b/extlib/DB/msql.php index c303bb9067..adcedf7a07 100644 --- a/extlib/DB/msql.php +++ b/extlib/DB/msql.php @@ -47,7 +47,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB * @since Class not functional until Release 1.7.0 */ @@ -126,13 +126,13 @@ class DB_msql extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_msql() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/mssql.php b/extlib/DB/mssql.php index e25caf144e..d68ebfa61e 100644 --- a/extlib/DB/mssql.php +++ b/extlib/DB/mssql.php @@ -49,7 +49,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_mssql extends DB_common @@ -179,13 +179,13 @@ class DB_mssql extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_mssql() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/mysql.php b/extlib/DB/mysql.php index 21132d62df..ffefbc49fd 100644 --- a/extlib/DB/mysql.php +++ b/extlib/DB/mysql.php @@ -41,7 +41,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_mysql extends DB_common @@ -162,13 +162,13 @@ class DB_mysql extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_mysql() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/mysqli.php b/extlib/DB/mysqli.php index 5f081f7c4f..4f56f0fdac 100644 --- a/extlib/DB/mysqli.php +++ b/extlib/DB/mysqli.php @@ -43,7 +43,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB * @since Class functional since Release 1.6.3 */ @@ -224,13 +224,13 @@ class DB_mysqli extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_mysqli() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} @@ -497,7 +497,11 @@ class DB_mysqli extends DB_common */ function freeResult($result) { - return is_resource($result) ? mysqli_free_result($result) : false; + if (! $result instanceof mysqli_result) { + return false; + } + mysqli_free_result($result); + return true; } // }}} @@ -1031,6 +1035,10 @@ class DB_mysqli extends DB_common ? $this->mysqli_types[$tmp->type] : 'unknown', // http://bugs.php.net/?id=36579 + // Doc Bug #36579: mysqli_fetch_field length handling + // https://bugs.php.net/bug.php?id=62426 + // Bug #62426: mysqli_fetch_field_direct returns incorrect + // length on UTF8 fields 'len' => $tmp->length, 'flags' => $flags, ); diff --git a/extlib/DB/oci8.php b/extlib/DB/oci8.php index 9685c60e0c..1ca7a04e22 100644 --- a/extlib/DB/oci8.php +++ b/extlib/DB/oci8.php @@ -47,7 +47,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_oci8 extends DB_common @@ -173,13 +173,13 @@ class DB_oci8 extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_oci8() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/odbc.php b/extlib/DB/odbc.php index 75d4fe74ff..a33406a654 100644 --- a/extlib/DB/odbc.php +++ b/extlib/DB/odbc.php @@ -44,7 +44,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_odbc extends DB_common @@ -153,13 +153,13 @@ class DB_odbc extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_odbc() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/pgsql.php b/extlib/DB/pgsql.php index adfd6bf0a4..098d9f040a 100644 --- a/extlib/DB/pgsql.php +++ b/extlib/DB/pgsql.php @@ -43,7 +43,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_pgsql extends DB_common @@ -148,13 +148,13 @@ class DB_pgsql extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_pgsql() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/sqlite.php b/extlib/DB/sqlite.php index 7adfb4c475..9c5c8b3523 100644 --- a/extlib/DB/sqlite.php +++ b/extlib/DB/sqlite.php @@ -47,7 +47,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_sqlite extends DB_common @@ -152,13 +152,13 @@ class DB_sqlite extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_sqlite() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/DB/storage.php b/extlib/DB/storage.php index 9ac23c825e..640d86f2b9 100644 --- a/extlib/DB/storage.php +++ b/extlib/DB/storage.php @@ -38,7 +38,7 @@ require_once 'DB.php'; * @author Stig Bakken * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_storage extends PEAR @@ -94,7 +94,7 @@ class DB_storage extends PEAR * a reference to this object * */ - function DB_storage($table, $keycolumn, &$dbh, $validator = null) + function __construct($table, $keycolumn, &$dbh, $validator = null) { $this->PEAR('DB_Error'); $this->_table = $table; diff --git a/extlib/DB/sybase.php b/extlib/DB/sybase.php index d87b18caab..14d054b246 100644 --- a/extlib/DB/sybase.php +++ b/extlib/DB/sybase.php @@ -46,7 +46,7 @@ require_once 'DB/common.php'; * @author Daniel Convissor * @copyright 1997-2007 The PHP Group * @license http://www.php.net/license/3_0.txt PHP License 3.0 - * @version Release: 1.8.2 + * @version Release: 1.9.2 * @link http://pear.php.net/package/DB */ class DB_sybase extends DB_common @@ -141,13 +141,13 @@ class DB_sybase extends DB_common // {{{ constructor /** - * This constructor calls $this->DB_common() + * This constructor calls parent::__construct() * * @return void */ - function DB_sybase() + function __construct() { - $this->DB_common(); + parent::__construct(); } // }}} diff --git a/extlib/HTMLPurifier/HTMLPurifier.includes.php b/extlib/HTMLPurifier/HTMLPurifier.includes.php index fdb58c2d37..e8bce5c850 100644 --- a/extlib/HTMLPurifier/HTMLPurifier.includes.php +++ b/extlib/HTMLPurifier/HTMLPurifier.includes.php @@ -7,7 +7,7 @@ * primary concern and you are using an opcode cache. PLEASE DO NOT EDIT THIS * FILE, changes will be overwritten the next time the script is run. * - * @version 4.7.0 + * @version 4.9.3 * * @warning * You must *not* include any other HTML Purifier files before this file, @@ -137,6 +137,8 @@ require 'HTMLPurifier/AttrTransform/SafeObject.php'; require 'HTMLPurifier/AttrTransform/SafeParam.php'; require 'HTMLPurifier/AttrTransform/ScriptRequired.php'; require 'HTMLPurifier/AttrTransform/TargetBlank.php'; +require 'HTMLPurifier/AttrTransform/TargetNoopener.php'; +require 'HTMLPurifier/AttrTransform/TargetNoreferrer.php'; require 'HTMLPurifier/AttrTransform/Textarea.php'; require 'HTMLPurifier/ChildDef/Chameleon.php'; require 'HTMLPurifier/ChildDef/Custom.php'; @@ -175,6 +177,8 @@ require 'HTMLPurifier/HTMLModule/StyleAttribute.php'; require 'HTMLPurifier/HTMLModule/Tables.php'; require 'HTMLPurifier/HTMLModule/Target.php'; require 'HTMLPurifier/HTMLModule/TargetBlank.php'; +require 'HTMLPurifier/HTMLModule/TargetNoopener.php'; +require 'HTMLPurifier/HTMLModule/TargetNoreferrer.php'; require 'HTMLPurifier/HTMLModule/Text.php'; require 'HTMLPurifier/HTMLModule/Tidy.php'; require 'HTMLPurifier/HTMLModule/XMLCommonAttributes.php'; @@ -225,5 +229,6 @@ require 'HTMLPurifier/URIScheme/https.php'; require 'HTMLPurifier/URIScheme/mailto.php'; require 'HTMLPurifier/URIScheme/news.php'; require 'HTMLPurifier/URIScheme/nntp.php'; +require 'HTMLPurifier/URIScheme/tel.php'; require 'HTMLPurifier/VarParser/Flexible.php'; require 'HTMLPurifier/VarParser/Native.php'; diff --git a/extlib/HTMLPurifier/HTMLPurifier.php b/extlib/HTMLPurifier/HTMLPurifier.php index c6041bc113..b4605ebc6e 100644 --- a/extlib/HTMLPurifier/HTMLPurifier.php +++ b/extlib/HTMLPurifier/HTMLPurifier.php @@ -19,7 +19,7 @@ */ /* - HTML Purifier 4.7.0 - Standards Compliant HTML Filtering + HTML Purifier 4.9.3 - Standards Compliant HTML Filtering Copyright (C) 2006-2008 Edward Z. Yang This library is free software; you can redistribute it and/or @@ -58,12 +58,12 @@ class HTMLPurifier * Version of HTML Purifier. * @type string */ - public $version = '4.7.0'; + public $version = '4.9.3'; /** * Constant with version of HTML Purifier. */ - const VERSION = '4.7.0'; + const VERSION = '4.9.3'; /** * Global configuration object. @@ -104,7 +104,7 @@ class HTMLPurifier /** * Initializes the purifier. * - * @param HTMLPurifier_Config $config Optional HTMLPurifier_Config object + * @param HTMLPurifier_Config|mixed $config Optional HTMLPurifier_Config object * for all instances of the purifier, if omitted, a default * configuration is supplied (which can be overridden on a * per-use basis). diff --git a/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php b/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php index 9dea6d1ed5..a3261f8a32 100644 --- a/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php +++ b/extlib/HTMLPurifier/HTMLPurifier.safe-includes.php @@ -131,6 +131,8 @@ require_once $__dir . '/HTMLPurifier/AttrTransform/SafeObject.php'; require_once $__dir . '/HTMLPurifier/AttrTransform/SafeParam.php'; require_once $__dir . '/HTMLPurifier/AttrTransform/ScriptRequired.php'; require_once $__dir . '/HTMLPurifier/AttrTransform/TargetBlank.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoopener.php'; +require_once $__dir . '/HTMLPurifier/AttrTransform/TargetNoreferrer.php'; require_once $__dir . '/HTMLPurifier/AttrTransform/Textarea.php'; require_once $__dir . '/HTMLPurifier/ChildDef/Chameleon.php'; require_once $__dir . '/HTMLPurifier/ChildDef/Custom.php'; @@ -169,6 +171,8 @@ require_once $__dir . '/HTMLPurifier/HTMLModule/StyleAttribute.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/Tables.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/Target.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/TargetBlank.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoopener.php'; +require_once $__dir . '/HTMLPurifier/HTMLModule/TargetNoreferrer.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/Text.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/Tidy.php'; require_once $__dir . '/HTMLPurifier/HTMLModule/XMLCommonAttributes.php'; @@ -219,5 +223,6 @@ require_once $__dir . '/HTMLPurifier/URIScheme/https.php'; require_once $__dir . '/HTMLPurifier/URIScheme/mailto.php'; require_once $__dir . '/HTMLPurifier/URIScheme/news.php'; require_once $__dir . '/HTMLPurifier/URIScheme/nntp.php'; +require_once $__dir . '/HTMLPurifier/URIScheme/tel.php'; require_once $__dir . '/HTMLPurifier/VarParser/Flexible.php'; require_once $__dir . '/HTMLPurifier/VarParser/Native.php'; diff --git a/extlib/HTMLPurifier/HTMLPurifier/Arborize.php b/extlib/HTMLPurifier/HTMLPurifier/Arborize.php index 9e6617be5d..d2e9d22a20 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/Arborize.php +++ b/extlib/HTMLPurifier/HTMLPurifier/Arborize.php @@ -19,8 +19,8 @@ class HTMLPurifier_Arborize if ($token instanceof HTMLPurifier_Token_End) { $token->start = null; // [MUT] $r = array_pop($stack); - assert($r->name === $token->name); - assert(empty($token->attr)); + //assert($r->name === $token->name); + //assert(empty($token->attr)); $r->endCol = $token->col; $r->endLine = $token->line; $r->endArmor = $token->armor; @@ -32,7 +32,7 @@ class HTMLPurifier_Arborize $stack[] = $node; } } - assert(count($stack) == 1); + //assert(count($stack) == 1); return $stack[0]; } diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php b/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php index 4f6c2e39a2..c7b17cf144 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrCollections.php @@ -21,6 +21,11 @@ class HTMLPurifier_AttrCollections * @param HTMLPurifier_HTMLModule[] $modules Hash array of HTMLPurifier_HTMLModule members */ public function __construct($attr_types, $modules) + { + $this->doConstruct($attr_types, $modules); + } + + public function doConstruct($attr_types, $modules) { // load extensions from the modules foreach ($modules as $module) { diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php index 5ac06522b9..739646fa7c 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef.php @@ -86,7 +86,13 @@ abstract class HTMLPurifier_AttrDef */ protected function mungeRgb($string) { - return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string); + $p = '\s*(\d+(\.\d+)?([%]?))\s*'; + + if (preg_match('/(rgba|hsla)\(/', $string)) { + return preg_replace('/(rgba|hsla)\('.$p.','.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8,\11)', $string); + } + + return preg_replace('/(rgb|hsl)\('.$p.','.$p.','.$p.'\)/', '\1(\2,\5,\8)', $string); } /** diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php index 02c1641fb2..ad2cb90ad1 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS.php @@ -25,15 +25,42 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef $css = $this->parseCDATA($css); $definition = $config->getCSSDefinition(); + $allow_duplicates = $config->get("CSS.AllowDuplicates"); - // we're going to break the spec and explode by semicolons. - // This is because semicolon rarely appears in escaped form - // Doing this is generally flaky but fast - // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI - // for details - $declarations = explode(';', $css); + // According to the CSS2.1 spec, the places where a + // non-delimiting semicolon can appear are in strings + // escape sequences. So here is some dumb hack to + // handle quotes. + $len = strlen($css); + $accum = ""; + $declarations = array(); + $quoted = false; + for ($i = 0; $i < $len; $i++) { + $c = strcspn($css, ";'\"", $i); + $accum .= substr($css, $i, $c); + $i += $c; + if ($i == $len) break; + $d = $css[$i]; + if ($quoted) { + $accum .= $d; + if ($d == $quoted) { + $quoted = false; + } + } else { + if ($d == ";") { + $declarations[] = $accum; + $accum = ""; + } else { + $accum .= $d; + $quoted = $d; + } + } + } + if ($accum != "") $declarations[] = $accum; + $propvalues = array(); + $new_declarations = ''; /** * Name of the current CSS property being validated. @@ -83,7 +110,11 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef if ($result === false) { continue; } - $propvalues[$property] = $result; + if ($allow_duplicates) { + $new_declarations .= "$property:$result;"; + } else { + $propvalues[$property] = $result; + } } $context->destroy('CurrentCSSProperty'); @@ -92,7 +123,6 @@ class HTMLPurifier_AttrDef_CSS extends HTMLPurifier_AttrDef // slightly inefficient, but it's the only way of getting rid of // duplicates. Perhaps config to optimize it, but not now. - $new_declarations = ''; foreach ($propvalues as $prop => $value) { $new_declarations .= "$prop:$value;"; } diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php index 16d2a6b98c..d7287a00c2 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/Color.php @@ -6,6 +6,16 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef { + /** + * @type HTMLPurifier_AttrDef_CSS_AlphaValue + */ + protected $alpha; + + public function __construct() + { + $this->alpha = new HTMLPurifier_AttrDef_CSS_AlphaValue(); + } + /** * @param string $color * @param HTMLPurifier_Config $config @@ -29,59 +39,104 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef return $colors[$lower]; } - if (strpos($color, 'rgb(') !== false) { - // rgb literal handling + if (preg_match('#(rgb|rgba|hsl|hsla)\(#', $color, $matches) === 1) { $length = strlen($color); if (strpos($color, ')') !== $length - 1) { return false; } - $triad = substr($color, 4, $length - 4 - 1); - $parts = explode(',', $triad); - if (count($parts) !== 3) { + + // get used function : rgb, rgba, hsl or hsla + $function = $matches[1]; + + $parameters_size = 3; + $alpha_channel = false; + if (substr($function, -1) === 'a') { + $parameters_size = 4; + $alpha_channel = true; + } + + /* + * Allowed types for values : + * parameter_position => [type => max_value] + */ + $allowed_types = array( + 1 => array('percentage' => 100, 'integer' => 255), + 2 => array('percentage' => 100, 'integer' => 255), + 3 => array('percentage' => 100, 'integer' => 255), + ); + $allow_different_types = false; + + if (strpos($function, 'hsl') !== false) { + $allowed_types = array( + 1 => array('integer' => 360), + 2 => array('percentage' => 100), + 3 => array('percentage' => 100), + ); + $allow_different_types = true; + } + + $values = trim(str_replace($function, '', $color), ' ()'); + + $parts = explode(',', $values); + if (count($parts) !== $parameters_size) { return false; } - $type = false; // to ensure that they're all the same type + + $type = false; $new_parts = array(); + $i = 0; + foreach ($parts as $part) { + $i++; $part = trim($part); + if ($part === '') { return false; } - $length = strlen($part); - if ($part[$length - 1] === '%') { - // handle percents - if (!$type) { - $type = 'percentage'; - } elseif ($type !== 'percentage') { + + // different check for alpha channel + if ($alpha_channel === true && $i === count($parts)) { + $result = $this->alpha->validate($part, $config, $context); + + if ($result === false) { return false; } - $num = (float)substr($part, 0, $length - 1); - if ($num < 0) { - $num = 0; - } - if ($num > 100) { - $num = 100; - } - $new_parts[] = "$num%"; + + $new_parts[] = (string)$result; + continue; + } + + if (substr($part, -1) === '%') { + $current_type = 'percentage'; } else { - // handle integers - if (!$type) { - $type = 'integer'; - } elseif ($type !== 'integer') { - return false; - } - $num = (int)$part; - if ($num < 0) { - $num = 0; - } - if ($num > 255) { - $num = 255; - } - $new_parts[] = (string)$num; + $current_type = 'integer'; + } + + if (!array_key_exists($current_type, $allowed_types[$i])) { + return false; + } + + if (!$type) { + $type = $current_type; + } + + if ($allow_different_types === false && $type != $current_type) { + return false; + } + + $max_value = $allowed_types[$i][$current_type]; + + if ($current_type == 'integer') { + // Return value between range 0 -> $max_value + $new_parts[] = (int)max(min($part, $max_value), 0); + } elseif ($current_type == 'percentage') { + $new_parts[] = (float)max(min(rtrim($part, '%'), $max_value), 0) . '%'; } } - $new_triad = implode(',', $new_parts); - $color = "rgb($new_triad)"; + + $new_values = implode(',', $new_parts); + + $color = $function . '(' . $new_values . ')'; } else { // hexadecimal handling if ($color[0] === '#') { @@ -100,6 +155,7 @@ class HTMLPurifier_AttrDef_CSS_Color extends HTMLPurifier_AttrDef } return $color; } + } // vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php index 86101020dc..74e24c8816 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php @@ -130,6 +130,8 @@ class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef // . See // the CSS3 spec for more examples: // + // You can see live samples of these on the Internet: + // // However, most of these fonts have ASCII equivalents: // for example, 'MS Mincho', and it's considered // professional to use ASCII font names instead of diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php index f9434230e2..6617acace5 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/CSS/URI.php @@ -33,6 +33,9 @@ class HTMLPurifier_AttrDef_CSS_URI extends HTMLPurifier_AttrDef_URI return false; } $uri_string = substr($uri_string, 4); + if (strlen($uri_string) == 0) { + return false; + } $new_length = strlen($uri_string) - 1; if ($uri_string[$new_length] != ')') { return false; diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php index 3d86efb44c..4ba45610fe 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/HTML/ID.php @@ -72,18 +72,26 @@ class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef // we purposely avoid using regex, hopefully this is faster - if (ctype_alpha($id)) { - $result = true; - } else { - if (!ctype_alpha(@$id[0])) { + if ($config->get('Attr.ID.HTML5') === true) { + if (preg_match('/[\t\n\x0b\x0c ]/', $id)) { return false; } - // primitive style of regexps, I suppose - $trim = trim( - $id, - 'A..Za..z0..9:-._' - ); - $result = ($trim === ''); + } else { + if (ctype_alpha($id)) { + // OK + } else { + if (!ctype_alpha(@$id[0])) { + return false; + } + // primitive style of regexps, I suppose + $trim = trim( + $id, + 'A..Za..z0..9:-._' + ); + if ($trim !== '') { + return false; + } + } } $regexp = $config->get('Attr.IDBlacklistRegexp'); @@ -91,14 +99,14 @@ class HTMLPurifier_AttrDef_HTML_ID extends HTMLPurifier_AttrDef return false; } - if (!$this->selector && $result) { + if (!$this->selector) { $id_accumulator->add($id); } // if no change was made to the ID, return the result // else, return the new id if stripping whitespace made it // valid, or return false. - return $result ? $id : false; + return $id; } } diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php index e7df800b1e..3b4d186743 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrDef/URI/Host.php @@ -76,24 +76,33 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef // fairly well supported. $underscore = $config->get('Core.AllowHostnameUnderscore') ? '_' : ''; + // Based off of RFC 1738, but amended so that + // as per RFC 3696, the top label need only not be all numeric. // The productions describing this are: $a = '[a-z]'; // alpha $an = '[a-z0-9]'; // alphanum $and = "[a-z0-9-$underscore]"; // alphanum | "-" // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum - $domainlabel = "$an($and*$an)?"; - // toplabel = alpha | alpha *( alphanum | "-" ) alphanum - $toplabel = "$a($and*$an)?"; + $domainlabel = "$an(?:$and*$an)?"; + // AMENDED as per RFC 3696 + // toplabel = alphanum | alphanum *( alphanum | "-" ) alphanum + // side condition: not all numeric + $toplabel = "$an(?:$and*$an)?"; // hostname = *( domainlabel "." ) toplabel [ "." ] - if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { - return $string; + if (preg_match("/^(?:$domainlabel\.)*($toplabel)\.?$/i", $string, $matches)) { + if (!ctype_digit($matches[1])) { + return $string; + } } + // PHP 5.3 and later support this functionality natively + if (function_exists('idn_to_ascii')) { + $string = idn_to_ascii($string); + // If we have Net_IDNA2 support, we can support IRIs by // punycoding them. (This is the most portable thing to do, // since otherwise we have to assume browsers support - - if ($config->get('Core.EnableIDNA')) { + } elseif ($config->get('Core.EnableIDNA')) { $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); // we need to encode each period separately $parts = explode('.', $string); @@ -114,13 +123,14 @@ class HTMLPurifier_AttrDef_URI_Host extends HTMLPurifier_AttrDef } } $string = implode('.', $new_parts); - if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { - return $string; - } } catch (Exception $e) { // XXX error reporting } } + // Try again + if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { + return $string; + } return false; } } diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php index 7df6cb3e1b..235ebb34b6 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/ImgRequired.php @@ -32,8 +32,7 @@ class HTMLPurifier_AttrTransform_ImgRequired extends HTMLPurifier_AttrTransform if ($src) { $alt = $config->get('Attr.DefaultImageAlt'); if ($alt === null) { - // truncate if the alt is too long - $attr['alt'] = substr(basename($attr['src']), 0, 40); + $attr['alt'] = basename($attr['src']); } else { $attr['alt'] = $alt; } diff --git a/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/TargetNoopener.php b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/TargetNoopener.php new file mode 100644 index 0000000000..1db3c6c09e --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/AttrTransform/TargetNoopener.php @@ -0,0 +1,37 @@ +get('CSS.MaxImgLength'); + $this->info['min-width'] = + $this->info['max-width'] = + $this->info['min-height'] = + $this->info['max-height'] = $this->info['width'] = $this->info['height'] = $max === null ? @@ -370,6 +374,19 @@ class HTMLPurifier_CSSDefinition extends HTMLPurifier_Definition ); $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto', 'avoid')); + $border_radius = new HTMLPurifier_AttrDef_CSS_Composite( + array( + new HTMLPurifier_AttrDef_CSS_Percentage(true), // disallow negative + new HTMLPurifier_AttrDef_CSS_Length('0') // disallow negative + )); + + $this->info['border-top-left-radius'] = + $this->info['border-top-right-radius'] = + $this->info['border-bottom-right-radius'] = + $this->info['border-bottom-left-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 2); + // TODO: support SLASH syntax + $this->info['border-radius'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_radius, 4); + } /** diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/List.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/List.php index 891b9f6f5b..4fc70e0efa 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/List.php +++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/List.php @@ -38,13 +38,19 @@ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef return false; } + // if li is not allowed, delete parent node + if (!isset($config->getHTMLDefinition()->info['li'])) { + trigger_error("Cannot allow ul/ol without allowing li", E_USER_WARNING); + return false; + } + // the new set of children $result = array(); // a little sanity check to make sure it's not ALL whitespace $all_whitespace = true; - $current_li = false; + $current_li = null; foreach ($children as $node) { if (!empty($node->is_whitespace)) { @@ -65,7 +71,7 @@ class HTMLPurifier_ChildDef_List extends HTMLPurifier_ChildDef // to handle non-list elements; non-list elements should // not be appended to an existing li; only li created // for non-list. This distinction is not currently made. - if ($current_li === false) { + if ($current_li === null) { $current_li = new HTMLPurifier_Node_Element('li'); $result[] = $current_li; } diff --git a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php index 3e4a0f2182..cb6b3e6cdc 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php +++ b/extlib/HTMLPurifier/HTMLPurifier/ChildDef/Table.php @@ -203,7 +203,7 @@ class HTMLPurifier_ChildDef_Table extends HTMLPurifier_ChildDef $current_tr_tbody->children[] = $node; break; case '#PCDATA': - assert($node->is_whitespace); + //assert($node->is_whitespace); if ($current_tr_tbody === null) { $ret[] = $node; } else { diff --git a/extlib/HTMLPurifier/HTMLPurifier/Config.php b/extlib/HTMLPurifier/HTMLPurifier/Config.php index 2b2db0c264..3648364b30 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/Config.php +++ b/extlib/HTMLPurifier/HTMLPurifier/Config.php @@ -21,7 +21,7 @@ class HTMLPurifier_Config * HTML Purifier's version * @type string */ - public $version = '4.7.0'; + public $version = '4.9.3'; /** * Whether or not to automatically finalize @@ -333,7 +333,7 @@ class HTMLPurifier_Config } // Raw type might be negative when using the fully optimized form - // of stdclass, which indicates allow_null == true + // of stdClass, which indicates allow_null == true $rtype = is_int($def) ? $def : $def->type; if ($rtype < 0) { $type = -$rtype; diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php index bfbb0f92f5..655c0e97ae 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema.php @@ -24,11 +24,11 @@ class HTMLPurifier_ConfigSchema * * array( * 'Namespace' => array( - * 'Directive' => new stdclass(), + * 'Directive' => new stdClass(), * ) * ) * - * The stdclass may have the following properties: + * The stdClass may have the following properties: * * - If isAlias isn't set: * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions @@ -39,8 +39,8 @@ class HTMLPurifier_ConfigSchema * - namespace: Namespace this directive aliases to * - name: Directive name this directive aliases to * - * In certain degenerate cases, stdclass will actually be an integer. In - * that case, the value is equivalent to an stdclass with the type + * In certain degenerate cases, stdClass will actually be an integer. In + * that case, the value is equivalent to an stdClass with the type * property set to the integer. If the integer is negative, type is * equal to the absolute value of integer, and allow_null is true. * @@ -105,7 +105,7 @@ class HTMLPurifier_ConfigSchema */ public function add($key, $default, $type, $allow_null) { - $obj = new stdclass(); + $obj = new stdClass(); $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; if ($allow_null) { $obj->allow_null = true; @@ -152,14 +152,14 @@ class HTMLPurifier_ConfigSchema */ public function addAlias($key, $new_key) { - $obj = new stdclass; + $obj = new stdClass; $obj->key = $new_key; $obj->isAlias = true; $this->info[$key] = $obj; } /** - * Replaces any stdclass that only has the type property with type integer. + * Replaces any stdClass that only has the type property with type integer. */ public function postProcess() { diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser index 1e6ccd2275..371e948f1c 100644 Binary files a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser and b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema.ser differ diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt new file mode 100644 index 0000000000..735d4b7a10 --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Attr.ID.HTML5.txt @@ -0,0 +1,10 @@ +Attr.ID.HTML5 +TYPE: bool/null +DEFAULT: null +VERSION: 4.8.0 +--DESCRIPTION-- +In HTML5, restrictions on the format of the id attribute have been significantly +relaxed, such that any string is valid so long as it contains no spaces and +is at least one character. In lieu of a general HTML5 compatibility flag, +set this configuration directive to true to use the relaxed rules. +--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt new file mode 100644 index 0000000000..4d054b1f07 --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowDuplicates.txt @@ -0,0 +1,11 @@ +CSS.AllowDuplicates +TYPE: bool +DEFAULT: false +VERSION: 4.8.0 +--DESCRIPTION-- +

+ By default, HTML Purifier removes duplicate CSS properties, + like color:red; color:blue. If this is set to + true, duplicate properties are allowed. +

+--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt index b2b83d9ab6..2e0cc81044 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt @@ -1,5 +1,5 @@ Cache.SerializerPermissions -TYPE: int +TYPE: int/null VERSION: 4.3.0 DEFAULT: 0755 --DESCRIPTION-- @@ -8,4 +8,9 @@ DEFAULT: 0755 Directory permissions of the files and directories created inside the DefinitionCache/Serializer or other custom serializer path.

+

+ In HTML Purifier 4.8.0, this also supports NULL, + which means that no chmod'ing or directory creation shall + occur. +

--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt new file mode 100644 index 0000000000..b2b6ab1496 --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyRemoveScript.txt @@ -0,0 +1,16 @@ +Core.AggressivelyRemoveScript +TYPE: bool +VERSION: 4.9.0 +DEFAULT: true +--DESCRIPTION-- +

+ This directive enables aggressive pre-filter removal of + script tags. This is not necessary for security, + but it can help work around a bug in libxml where embedded + HTML elements inside script sections cause the parser to + choke. To revert to pre-4.9.0 behavior, set this to false. + This directive has no effect if %Core.Trusted is true, + %Core.RemoveScriptContents is false, or %Core.HiddenElements + does not contain script. +

+--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt new file mode 100644 index 0000000000..392b436493 --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/Core.LegacyEntityDecoder.txt @@ -0,0 +1,36 @@ +Core.LegacyEntityDecoder +TYPE: bool +VERSION: 4.9.0 +DEFAULT: false +--DESCRIPTION-- +

+ Prior to HTML Purifier 4.9.0, entities were decoded by performing + a global search replace for all entities whose decoded versions + did not have special meanings under HTML, and replaced them with + their decoded versions. We would match all entities, even if they did + not have a trailing semicolon, but only if there weren't any trailing + alphanumeric characters. +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena&yena&yena
&yen=¥=¥=
+

+ In HTML Purifier 4.9.0, we changed the behavior of entity parsing + to match entities that had missing trailing semicolons in less + cases, to more closely match HTML5 parsing behavior: +

+ + + + + + +
OriginalTextAttribute
&yen;¥¥
&yen¥¥
&yena¥a&yena
&yen=¥=&yen=
+

+ This flag reverts back to pre-HTML Purifier 4.9.0 behavior. +

+--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt new file mode 100644 index 0000000000..dd514c0def --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoopener.txt @@ -0,0 +1,10 @@ +--# vim: et sw=4 sts=4 +HTML.TargetNoopener +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noopener rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt new file mode 100644 index 0000000000..cb5a0b0e5e --- /dev/null +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetNoreferrer.txt @@ -0,0 +1,9 @@ +HTML.TargetNoreferrer +TYPE: bool +VERSION: 4.8.0 +DEFAULT: TRUE +--DESCRIPTION-- +If enabled, noreferrer rel attributes are added to links which have +a target attribute associated with them. This prevents malicious +destinations from overwriting the original window. +--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt index 666635a5ff..eb97307e20 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt @@ -8,6 +8,7 @@ array ( 'ftp' => true, 'nntp' => true, 'news' => true, + 'tel' => true, ) --DESCRIPTION-- Whitelist that defines the schemes that a URI is allowed to have. This diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt index 728e378cbe..834bc08c0b 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt @@ -1,5 +1,5 @@ URI.DefaultScheme -TYPE: string +TYPE: string/null DEFAULT: 'http' --DESCRIPTION-- @@ -7,4 +7,9 @@ DEFAULT: 'http' Defines through what scheme the output will be served, in order to select the proper object validator when no scheme information is present.

+ +

+ Starting with HTML Purifier 4.9.0, the default scheme can be null, in + which case we reject all URIs which do not have explicit schemes. +

--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt index 179f94eb03..58c81dcc44 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt +++ b/extlib/HTMLPurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt @@ -9,75 +9,75 @@ DEFAULT: NULL absolute URIs into another URI, usually a URI redirection service. This directive accepts a URI, formatted with a %s where the url-encoded original URI should be inserted (sample: - https://searx.laquadrature.net/?q=%s). -

-

+ http://www.google.com/url?q=%s). +

+

Uses for this directive: -

-
    +

    +
    • - Prevent PageRank leaks, while being fairly transparent - to users (you may also want to add some client side JavaScript to - override the text in the statusbar). Notice: - Many security experts believe that this form of protection does not deter spam-bots. + Prevent PageRank leaks, while being fairly transparent + to users (you may also want to add some client side JavaScript to + override the text in the statusbar). Notice: + Many security experts believe that this form of protection does not deter spam-bots.
    • - Redirect users to a splash page telling them they are leaving your - website. While this is poor usability practice, it is often mandated - in corporate environments. + Redirect users to a splash page telling them they are leaving your + website. While this is poor usability practice, it is often mandated + in corporate environments.
    • -
    -

    +

+

Prior to HTML Purifier 3.1.1, this directive also enabled the munging of browsable external resources, which could break things if your redirection script was a splash page or used meta tags. To revert to previous behavior, please use %URI.MungeResources. -

-

+

+

You may want to also use %URI.MungeSecretKey along with this directive in order to enforce what URIs your redirector script allows. Open redirector scripts can be a security risk and negatively affect the reputation of your domain name. -

-

+

+

Starting with HTML Purifier 3.1.1, there is also these substitutions: -

- +

+
- - - - - + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + -
KeyDescriptionExample <a href="">
KeyDescriptionExample <a href="">
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe name of the CSS property this URI came from, or blank if irrelevant
%r1 - The URI embeds a resource
(blank) - The URI is merely a link
%nThe name of the tag this URI came froma
%mThe name of the attribute this URI came fromhref
%pThe name of the CSS property this URI came from, or blank if irrelevant
-

+ +

Admittedly, these letters are somewhat arbitrary; the only stipulation was that they couldn't be a through f. r is for resource (I would have preferred e, but you take what you can get), n is for name, m was picked because it came after n (and I couldn't use a), p is for property. -

- --# vim: et sw=4 sts=4 +

+--# vim: et sw=4 sts=4 diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php index 67bb5b1e69..9aa8ff354f 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php +++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache.php @@ -118,7 +118,7 @@ abstract class HTMLPurifier_DefinitionCache /** * Clears all expired (older version or revision) objects from cache - * @note Be carefuly implementing this method as flush. Flush must + * @note Be careful implementing this method as flush. Flush must * not interfere with other Definition types, and cleanup() * should not be repeatedly called by userland code. * @param HTMLPurifier_Config $config diff --git a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php index ce268d91b4..952e48d470 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php +++ b/extlib/HTMLPurifier/HTMLPurifier/DefinitionCache/Serializer.php @@ -97,6 +97,12 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac } $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); + // Apparently, on some versions of PHP, readdir will return + // an empty string if you pass an invalid argument to readdir. + // So you need this test. See #49. + if (false === $dh) { + return false; + } while (false !== ($filename = readdir($dh))) { if (empty($filename)) { continue; @@ -106,6 +112,8 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac } unlink($dir . '/' . $filename); } + closedir($dh); + return true; } /** @@ -119,6 +127,10 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac } $dir = $this->generateDirectoryPath($config); $dh = opendir($dir); + // See #49 (and above). + if (false === $dh) { + return false; + } while (false !== ($filename = readdir($dh))) { if (empty($filename)) { continue; @@ -131,6 +143,8 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac unlink($dir . '/' . $filename); } } + closedir($dh); + return true; } /** @@ -186,11 +200,9 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac if ($result !== false) { // set permissions of the new file (no execute) $chmod = $config->get('Cache.SerializerPermissions'); - if (!$chmod) { - $chmod = 0644; // invalid config or simpletest + if ($chmod !== null) { + chmod($file, $chmod & 0666); } - $chmod = $chmod & 0666; - chmod($file, $chmod); } return $result; } @@ -204,8 +216,10 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac { $directory = $this->generateDirectoryPath($config); $chmod = $config->get('Cache.SerializerPermissions'); - if (!$chmod) { - $chmod = 0755; // invalid config or simpletest + if ($chmod === null) { + // TODO: This races + if (is_dir($directory)) return true; + return mkdir($directory); } if (!is_dir($directory)) { $base = $this->generateBaseDirectoryPath($config); @@ -219,15 +233,16 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac } elseif (!$this->_testPermissions($base, $chmod)) { return false; } - mkdir($directory, $chmod); - if (!$this->_testPermissions($directory, $chmod)) { + if (!mkdir($directory, $chmod)) { trigger_error( - 'Base directory ' . $base . ' does not exist, - please create or change using %Cache.SerializerPath', + 'Could not create directory ' . $directory . '', E_USER_WARNING ); return false; } + if (!$this->_testPermissions($directory, $chmod)) { + return false; + } } elseif (!$this->_testPermissions($directory, $chmod)) { return false; } @@ -256,7 +271,7 @@ class HTMLPurifier_DefinitionCache_Serializer extends HTMLPurifier_DefinitionCac ); return false; } - if (function_exists('posix_getuid')) { + if (function_exists('posix_getuid') && $chmod !== null) { // POSIX system, we can give more specific advice if (fileowner($dir) === posix_getuid()) { // we can chmod it ourselves diff --git a/extlib/HTMLPurifier/HTMLPurifier/Encoder.php b/extlib/HTMLPurifier/HTMLPurifier/Encoder.php index fef9b58906..b94f175423 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/Encoder.php +++ b/extlib/HTMLPurifier/HTMLPurifier/Encoder.php @@ -101,6 +101,14 @@ class HTMLPurifier_Encoder * It will parse according to UTF-8 and return a valid UTF8 string, with * non-SGML codepoints excluded. * + * Specifically, it will permit: + * \x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF} + * Source: https://www.w3.org/TR/REC-xml/#NT-Char + * Arguably this function should be modernized to the HTML5 set + * of allowed characters: + * https://www.w3.org/TR/html5/syntax.html#preprocessing-the-input-stream + * which simultaneously expand and restrict the set of allowed characters. + * * @param string $str The string to clean * @param bool $force_php * @return string @@ -122,15 +130,12 @@ class HTMLPurifier_Encoder * function that needs to be able to understand UTF-8 characters. * As of right now, only smart lossless character encoding converters * would need that, and I'm probably not going to implement them. - * Once again, PHP 6 should solve all our problems. */ public static function cleanUTF8($str, $force_php = false) { // UTF-8 validity is checked since PHP 4.3.5 // This is an optimization: if the string is already valid UTF-8, no // need to do PHP stuff. 99% of the time, this will be the case. - // The regexp matches the XML char production, as well as well as excluding - // non-SGML codepoints U+007F to U+009F if (preg_match( '/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str @@ -255,6 +260,7 @@ class HTMLPurifier_Encoder // 7F-9F is not strictly prohibited by XML, // but it is non-SGML, and thus we don't allow it (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || + (0xE000 <= $mUcs4 && 0xFFFD >= $mUcs4) || (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) ) ) { diff --git a/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php b/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php index 61529dcd9d..c372b5a6a6 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php +++ b/extlib/HTMLPurifier/HTMLPurifier/EntityParser.php @@ -16,6 +16,138 @@ class HTMLPurifier_EntityParser */ protected $_entity_lookup; + /** + * Callback regex string for entities in text. + * @type string + */ + protected $_textEntitiesRegex; + + /** + * Callback regex string for entities in attributes. + * @type string + */ + protected $_attrEntitiesRegex; + + /** + * Tests if the beginning of a string is a semi-optional regex + */ + protected $_semiOptionalPrefixRegex; + + public function __construct() { + // From + // http://stackoverflow.com/questions/15532252/why-is-reg-being-rendered-as-without-the-bounding-semicolon + $semi_optional = "quot|QUOT|lt|LT|gt|GT|amp|AMP|AElig|Aacute|Acirc|Agrave|Aring|Atilde|Auml|COPY|Ccedil|ETH|Eacute|Ecirc|Egrave|Euml|Iacute|Icirc|Igrave|Iuml|Ntilde|Oacute|Ocirc|Ograve|Oslash|Otilde|Ouml|REG|THORN|Uacute|Ucirc|Ugrave|Uuml|Yacute|aacute|acirc|acute|aelig|agrave|aring|atilde|auml|brvbar|ccedil|cedil|cent|copy|curren|deg|divide|eacute|ecirc|egrave|eth|euml|frac12|frac14|frac34|iacute|icirc|iexcl|igrave|iquest|iuml|laquo|macr|micro|middot|nbsp|not|ntilde|oacute|ocirc|ograve|ordf|ordm|oslash|otilde|ouml|para|plusmn|pound|raquo|reg|sect|shy|sup1|sup2|sup3|szlig|thorn|times|uacute|ucirc|ugrave|uml|uuml|yacute|yen|yuml"; + + // NB: three empty captures to put the fourth match in the right + // place + $this->_semiOptionalPrefixRegex = "/&()()()($semi_optional)/"; + + $this->_textEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + "($semi_optional)". + ')/'; + + $this->_attrEntitiesRegex = + '/&(?:'. + // hex + '[#]x([a-fA-F0-9]+);?|'. + // dec + '[#]0*(\d+);?|'. + // string (mandatory semicolon) + // NB: order matters: match semicolon preferentially + '([A-Za-z_:][A-Za-z0-9.\-_:]*);|'. + // string (optional semicolon) + // don't match if trailing is equals or alphanumeric (URL + // like) + "($semi_optional)(?![=;A-Za-z0-9])". + ')/'; + + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * textual data in an HTML document (as opposed to attributes.) + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteTextEntities($string) + { + return preg_replace_callback( + $this->_textEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Substitute entities with the parsed equivalents. Use this on + * attribute contents in documents. + * + * @param string $string String to have entities parsed. + * @return string Parsed string. + */ + public function substituteAttrEntities($string) + { + return preg_replace_callback( + $this->_attrEntitiesRegex, + array($this, 'entityCallback'), + $string + ); + } + + /** + * Callback function for substituteNonSpecialEntities() that does the work. + * + * @param array $matches PCRE matches array, with 0 the entire match, and + * either index 1, 2 or 3 set with a hex value, dec value, + * or string (respectively). + * @return string Replacement string. + */ + + protected function entityCallback($matches) + { + $entity = $matches[0]; + $hex_part = @$matches[1]; + $dec_part = @$matches[2]; + $named_part = empty($matches[3]) ? @$matches[4] : $matches[3]; + if ($hex_part !== NULL && $hex_part !== "") { + return HTMLPurifier_Encoder::unichr(hexdec($hex_part)); + } elseif ($dec_part !== NULL && $dec_part !== "") { + return HTMLPurifier_Encoder::unichr((int) $dec_part); + } else { + if (!$this->_entity_lookup) { + $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); + } + if (isset($this->_entity_lookup->table[$named_part])) { + return $this->_entity_lookup->table[$named_part]; + } else { + // exact match didn't match anything, so test if + // any of the semicolon optional match the prefix. + // Test that this is an EXACT match is important to + // prevent infinite loop + if (!empty($matches[3])) { + return preg_replace_callback( + $this->_semiOptionalPrefixRegex, + array($this, 'entityCallback'), + $entity + ); + } + return $entity; + } + } + } + + // LEGACY CODE BELOW + /** * Callback regex string for parsing entities. * @type string @@ -144,7 +276,7 @@ class HTMLPurifier_EntityParser $entity; } else { return isset($this->_special_ent2dec[$matches[3]]) ? - $this->_special_ent2dec[$matches[3]] : + $this->_special_dec2str[$this->_special_ent2dec[$matches[3]]] : $entity; } } diff --git a/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php b/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php index 08e62c16bf..66f70b0fc0 100644 --- a/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php +++ b/extlib/HTMLPurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php @@ -95,7 +95,10 @@ class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter if ($tidy !== null) { $this->_tidy = $tidy; } - $html = preg_replace_callback('#(.+)#isU', array($this, 'styleCallback'), $html); + // NB: this must be NON-greedy because if we have + // + // we must not grab foo