diff --git a/actions/apifavoritecreate.php b/actions/apifavoritecreate.php index 3618f94018..00b6349b0a 100644 --- a/actions/apifavoritecreate.php +++ b/actions/apifavoritecreate.php @@ -123,7 +123,7 @@ class ApiFavoriteCreateAction extends ApiAuthAction return; } - $fave = Fave::addNew($this->user, $this->notice); + $fave = Fave::addNew($this->user->getProfile(), $this->notice); if (empty($fave)) { $this->clientError( diff --git a/actions/foaf.php b/actions/foaf.php index fc2ec9b12f..fc56e19b4f 100644 --- a/actions/foaf.php +++ b/actions/foaf.php @@ -162,40 +162,29 @@ class FoafAction extends Action if ($sub->find()) { while ($sub->fetch()) { - if ($sub->token) { - $other = Remote_profile::staticGet('id', $sub->subscriber); - $profile = Profile::staticGet('id', $sub->subscriber); - } else { - $other = User::staticGet('id', $sub->subscriber); - $profile = Profile::staticGet('id', $sub->subscriber); - } - if (!$other) { + $profile = Profile::staticGet('id', $sub->subscriber); + if (empty($profile)) { common_debug('Got a bad subscription: '.print_r($sub,true)); continue; } - if (array_key_exists($other->uri, $person)) { - $person[$other->uri][0] = BOTH; + $user = $profile->getUser(); + $other_uri = $profile->getUri(); + if (array_key_exists($other_uri, $person)) { + $person[$other_uri][0] = BOTH; } else { - $person[$other->uri] = array(LISTENER, - $other->id, - $profile->nickname, - (empty($sub->token)) ? 'User' : 'Remote_profile'); + $person[$other_uri] = array(LISTENER, + $profile->id, + $profile->nickname, + $user ? 'local' : 'remote'); } - $other->free(); - $other = null; - unset($other); - $profile->free(); - $profile = null; unset($profile); } } - $sub->free(); - $sub = null; unset($sub); foreach ($person as $uri => $p) { - list($type, $id, $nickname, $cls) = $p; + list($type, $id, $nickname, $local) = $p; if ($type == BOTH) { $this->element('knows', array('rdf:resource' => $uri)); } @@ -206,8 +195,8 @@ class FoafAction extends Action foreach ($person as $uri => $p) { $foaf_url = null; - list($type, $id, $nickname, $cls) = $p; - if ($cls == 'User') { + list($type, $id, $nickname, $local) = $p; + if ($local == 'local') { $foaf_url = common_local_url('foaf', array('nickname' => $nickname)); } $profile = Profile::staticGet($id); @@ -216,7 +205,7 @@ class FoafAction extends Action $this->element('knows', array('rdf:resource' => $this->user->uri)); } $this->showMicrobloggingAccount($profile, - ($cls == 'User') ? common_root_url() : null, + ($local == 'local') ? common_root_url() : null, $uri, true); if ($foaf_url) { @@ -275,33 +264,22 @@ class FoafAction extends Action if ($sub->find()) { while ($sub->fetch()) { - if (!empty($sub->token)) { - $other = Remote_profile::staticGet('id', $sub->subscribed); - $profile = Profile::staticGet('id', $sub->subscribed); - } else { - $other = User::staticGet('id', $sub->subscribed); - $profile = Profile::staticGet('id', $sub->subscribed); - } - if (empty($other)) { + $profile = Profile::staticGet('id', $sub->subscribed); + if (empty($profile)) { common_debug('Got a bad subscription: '.print_r($sub,true)); continue; } - $this->element('sioc:follows', array('rdf:resource' => $other->uri.'#acct')); - $person[$other->uri] = array(LISTENEE, - $other->id, - $profile->nickname, - (empty($sub->token)) ? 'User' : 'Remote_profile'); - $other->free(); - $other = null; - unset($other); - $profile->free(); - $profile = null; + $user = $profile->getUser(); + $other_uri = $profile->getUri(); + $this->element('sioc:follows', array('rdf:resource' => $other_uri.'#acct')); + $person[$other_uri] = array(LISTENEE, + $profile->id, + $profile->nickname, + $user ? 'local' : 'remote'); unset($profile); } } - $sub->free(); - $sub = null; unset($sub); } diff --git a/classes/Conversation.php b/classes/Conversation.php index ea8bd87b56..f540004ef3 100755 --- a/classes/Conversation.php +++ b/classes/Conversation.php @@ -63,7 +63,8 @@ class Conversation extends Memcached_DataObject } $orig = clone($conv); - $orig->uri = common_local_url('conversation', array('id' => $id)); + $orig->uri = common_local_url('conversation', array('id' => $id), + null, null, false); $result = $orig->update($conv); if (empty($result)) { diff --git a/classes/Fave.php b/classes/Fave.php index a04f15e9c4..7ca9ade7f0 100644 --- a/classes/Fave.php +++ b/classes/Fave.php @@ -21,7 +21,15 @@ class Fave extends Memcached_DataObject /* the code above is auto generated do not remove the tag below */ ###END_AUTOCODE - static function addNew($profile, $notice) { + /** + * Save a favorite record. + * @fixme post-author notification should be moved here + * + * @param Profile $profile the local or remote user who likes + * @param Notice $notice the notice that is liked + * @return mixed false on failure, or Fave record on success + */ + static function addNew(Profile $profile, Notice $notice) { $fave = null; diff --git a/classes/Notice.php b/classes/Notice.php index f7194e3394..be3e9ca2a6 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -421,7 +421,9 @@ class Notice extends Memcached_DataObject } $profile = Profile::staticGet($this->profile_id); - $profile->blowNoticeCount(); + if (!empty($profile)) { + $profile->blowNoticeCount(); + } } /** diff --git a/lib/apiaction.php b/lib/apiaction.php index e6aaf93161..9fc1a07799 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -1273,7 +1273,7 @@ class ApiAction extends Action if (empty($local)) { return null; } else { - return User_group::staticGet('id', $local->id); + return User_group::staticGet('id', $local->group_id); } } diff --git a/lib/command.php b/lib/command.php index f7421269d0..216f9e649a 100644 --- a/lib/command.php +++ b/lib/command.php @@ -273,7 +273,7 @@ class FavCommand extends Command function handle($channel) { $notice = $this->getNotice($this->other); - $fave = Fave::addNew($this->user, $notice); + $fave = Fave::addNew($this->user->getProfile(), $notice); if (!$fave) { $channel->error($this->user, _('Could not create favorite.')); diff --git a/lib/util.php b/lib/util.php index a30d691002..7959978683 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1529,7 +1529,8 @@ function common_user_uri(&$user) function common_notice_uri(&$notice) { return common_local_url('shownotice', - array('notice' => $notice->id)); + array('notice' => $notice->id), + null, null, false); } // 36 alphanums - lookalikes (0, O, 1, I) = 32 chars = 5 bits diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php index ecdcfa1939..6c360c49f9 100644 --- a/plugins/OStatus/actions/usersalmon.php +++ b/plugins/OStatus/actions/usersalmon.php @@ -92,7 +92,7 @@ class UsersalmonAction extends SalmonAction throw new ClientException("Not to anyone in reply to anything!"); } - $existing = Notice::staticGet('uri', $this->act->object->id); + $existing = Notice::staticGet('uri', $this->act->objects[0]->id); if (!empty($existing)) { common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists."); @@ -143,7 +143,7 @@ class UsersalmonAction extends SalmonAction function handleFavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $old = Fave::pkeyGet(array('user_id' => $profile->id, @@ -164,7 +164,7 @@ class UsersalmonAction extends SalmonAction */ function handleUnfavorite() { - $notice = $this->getNotice($this->act->object); + $notice = $this->getNotice($this->act->objects[0]); $profile = $this->ensureProfile()->localProfile(); $fave = Fave::pkeyGet(array('user_id' => $profile->id, diff --git a/plugins/OStatus/classes/Magicsig.php b/plugins/OStatus/classes/Magicsig.php index 87c684c93d..864fef6285 100644 --- a/plugins/OStatus/classes/Magicsig.php +++ b/plugins/OStatus/classes/Magicsig.php @@ -27,6 +27,8 @@ * @link http://status.net/ */ +require_once 'Crypt/RSA.php'; + class Magicsig extends Memcached_DataObject { @@ -50,7 +52,15 @@ class Magicsig extends Memcached_DataObject { $obj = parent::staticGet(__CLASS__, $k, $v); if (!empty($obj)) { - return Magicsig::fromString($obj->keypair); + $obj = Magicsig::fromString($obj->keypair); + + // Double check keys: Crypt_RSA did not + // consistently generate good keypairs. + // We've also moved to 1024 bit keys. + if (strlen($obj->publicKey->modulus->toBits()) != 1024) { + $obj->delete(); + return false; + } } return $obj; @@ -100,16 +110,16 @@ class Magicsig extends Memcached_DataObject public function generate($user_id) { - $rsa = new SafeCrypt_RSA(); + $rsa = new Crypt_RSA(); $keypair = $rsa->createKey(); $rsa->loadKey($keypair['privatekey']); - $this->privateKey = new SafeCrypt_RSA(); + $this->privateKey = new Crypt_RSA(); $this->privateKey->loadKey($keypair['privatekey']); - $this->publicKey = new SafeCrypt_RSA(); + $this->publicKey = new Crypt_RSA(); $this->publicKey->loadKey($keypair['publickey']); $this->user_id = $user_id; @@ -161,7 +171,7 @@ class Magicsig extends Memcached_DataObject { common_log(LOG_DEBUG, "Adding ".$type." key: (".$mod .', '. $exp .")"); - $rsa = new SafeCrypt_RSA(); + $rsa = new Crypt_RSA(); $rsa->signatureMode = CRYPT_RSA_SIGNATURE_PKCS1; $rsa->setHash('sha256'); $rsa->modulus = new Math_BigInteger(base64_url_decode($mod), 256); diff --git a/plugins/OStatus/extlib/Math/BigInteger.php b/plugins/OStatus/extlib/Math/BigInteger.php index 9733351d42..4373805f99 100644 --- a/plugins/OStatus/extlib/Math/BigInteger.php +++ b/plugins/OStatus/extlib/Math/BigInteger.php @@ -67,7 +67,7 @@ * @author Jim Wigginton * @copyright MMVI Jim Wigginton * @license http://www.gnu.org/licenses/lgpl.txt - * @version $Id: BigInteger.php,v 1.31 2010/03/01 17:28:19 terrafrost Exp $ + * @version $Id: BigInteger.php,v 1.33 2010/03/22 22:32:03 terrafrost Exp $ * @link http://pear.php.net/package/Math_BigInteger */ @@ -294,7 +294,7 @@ class Math_BigInteger { $this->value = array(); } - if ($x === 0) { + if (empty($x)) { return; } @@ -718,7 +718,7 @@ class Math_BigInteger { * * Will be called, automatically, when serialize() is called on a Math_BigInteger object. * - * @see __wakeup + * @see __wakeup() * @access public */ function __sleep() @@ -740,7 +740,7 @@ class Math_BigInteger { * * Will be called, automatically, when unserialize() is called on a Math_BigInteger object. * - * @see __sleep + * @see __sleep() * @access public */ function __wakeup() diff --git a/plugins/OStatus/lib/magicenvelope.php b/plugins/OStatus/lib/magicenvelope.php index 9266cab5cf..799b5e3079 100644 --- a/plugins/OStatus/lib/magicenvelope.php +++ b/plugins/OStatus/lib/magicenvelope.php @@ -59,12 +59,21 @@ class MagicEnvelope } if ($xrd->links) { if ($link = Discovery::getService($xrd->links, Magicsig::PUBLICKEYREL)) { - list($type, $keypair) = explode(',', $link['href']); - if (empty($keypair)) { + $keypair = false; + $parts = explode(',', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } else { // Backwards compatibility check for separator bug in 0.9.0 - list($type, $keypair) = explode(';', $link['href']); + $parts = explode(';', $link['href']); + if (count($parts) == 2) { + $keypair = $parts[1]; + } + } + + if ($keypair) { + return $keypair; } - return $keypair; } } throw new Exception('Unable to locate signer public key'); diff --git a/plugins/OStatus/lib/safecrypt_rsa.php b/plugins/OStatus/lib/safecrypt_rsa.php deleted file mode 100644 index f3aa2c9285..0000000000 --- a/plugins/OStatus/lib/safecrypt_rsa.php +++ /dev/null @@ -1,18 +0,0 @@ -zero = new SafeMath_BigInteger(); - } -} - diff --git a/plugins/OStatus/lib/safemath_biginteger.php b/plugins/OStatus/lib/safemath_biginteger.php deleted file mode 100644 index c05e24d1ec..0000000000 --- a/plugins/OStatus/lib/safemath_biginteger.php +++ /dev/null @@ -1,20 +0,0 @@ -hex == '') { - $this->hex = '0'; - } - parent::__wakeup(); - } -} - diff --git a/plugins/OStatus/lib/xrd.php b/plugins/OStatus/lib/xrd.php index aa13ef0242..34b28790b7 100644 --- a/plugins/OStatus/lib/xrd.php +++ b/plugins/OStatus/lib/xrd.php @@ -53,7 +53,14 @@ class XRD $xrd = new XRD(); $dom = new DOMDocument(); - if (!$dom->loadXML($xml)) { + + // Don't spew XML warnings to output + $old = error_reporting(); + error_reporting($old & ~E_WARNING); + $ok = $dom->loadXML($xml); + error_reporting($old); + + if (!$ok) { throw new Exception("Invalid XML"); } $xrd_element = $dom->getElementsByTagName('XRD')->item(0); diff --git a/plugins/OStatus/tests/remote-tests.php b/plugins/OStatus/tests/remote-tests.php index 103ca066c0..24b4b1660a 100644 --- a/plugins/OStatus/tests/remote-tests.php +++ b/plugins/OStatus/tests/remote-tests.php @@ -40,6 +40,20 @@ class TestBase } return true; } + + function assertTrue($a) + { + if (!$a) { + throw new Exception("Failed to assert true: got false"); + } + } + + function assertFalse($a) + { + if ($a) { + throw new Exception("Failed to assert false: got true"); + } + } } class OStatusTester extends TestBase @@ -60,8 +74,17 @@ class OStatusTester extends TestBase function run() { $this->setup(); - $this->testLocalPost(); - $this->testMentionUrl(); + + $methods = get_class_methods($this); + foreach ($methods as $method) { + if (strtolower(substr($method, 0, 4)) == 'test') { + print "\n"; + print "== $method ==\n"; + call_user_func(array($this, $method)); + } + } + + print "\n"; $this->log("DONE!"); } @@ -98,6 +121,45 @@ class OStatusTester extends TestBase $post = $this->pub->post("@$base/$name should have this in home and replies"); $this->sub->assertReceived($post); } + + function testSubscribe() + { + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->subscribe($this->pub->getProfileLink()); + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + + function testPush() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + + $name = $this->sub->username; + $post = $this->pub->post("Regular post, which $name should get via PuSH"); + $this->sub->assertReceived($post); + } + + function testMentionSubscribee() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscription($this->sub->getProfileUri())); + + $name = $this->pub->username; + $post = $this->sub->post("Just a quick note back to my remote subscribee @$name"); + $this->pub->assertReceived($post); + } + + function testUnsubscribe() + { + $this->assertTrue($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertTrue($this->pub->hasSubscriber($this->sub->getProfileUri())); + $this->sub->unsubscribe($this->pub->getProfileLink()); + $this->assertFalse($this->sub->hasSubscription($this->pub->getProfileUri())); + $this->assertFalse($this->pub->hasSubscriber($this->sub->getProfileUri())); + } + } class SNTestClient extends TestBase @@ -202,6 +264,43 @@ class SNTestClient extends TestBase return $dom; } + protected function parseXml($path, $body) + { + $dom = new DOMDocument(); + if ($dom->loadXML($body)) { + return $dom; + } else { + throw new Exception("Bogus XML data from $path:\n$body"); + } + } + + /** + * Make a hit to a REST-y XML page on the site, without authentication. + * @param string $path URL fragment for something relative to base + * @param array $params POST parameters to send + * @return DOMDocument + * @throws Exception on low-level error conditions + */ + protected function xml($path, $params=array()) + { + $response = $this->hit($path, $params, true); + $body = $response->getBody(); + return $this->parseXml($path, $body); + } + + protected function parseJson($path, $body) + { + $data = json_decode($body, true); + if ($data !== null) { + if (!empty($data['error'])) { + throw new Exception("JSON API returned error: " . $data['error']); + } + return $data; + } else { + throw new Exception("Bogus JSON data from $path:\n$body"); + } + } + /** * Make an API hit to this site, with authentication. * @param string $path URL fragment for something under 'api' folder @@ -215,22 +314,9 @@ class SNTestClient extends TestBase $response = $this->hit("api/$path.$style", $params, true); $body = $response->getBody(); if ($style == 'json') { - $data = json_decode($body, true); - if ($data !== null) { - if (!empty($data['error'])) { - throw new Exception("JSON API returned error: " . $data['error']); - } - return $data; - } else { - throw new Exception("Bogus JSON data from $path:\n$body"); - } + return $this->parseJson($path, $body); } else if ($style == 'xml' || $style == 'atom') { - $dom = new DOMDocument(); - if ($dom->loadXML($body)) { - return $dom; - } else { - throw new Exception("Bogus XML data from $path:\n$body"); - } + return $this->parseXml($path, $body); } else { throw new Exception("API needs to be JSON, XML, or Atom"); } @@ -257,6 +343,24 @@ class SNTestClient extends TestBase 'submit' => 'Register')); } + /** + * @return string canonical URI/URL to profile page + */ + function getProfileUri() + { + $data = $this->api('account/verify_credentials', 'json'); + $id = $data['id']; + return $this->basepath . '/user/' . $id; + } + + /** + * @return string human-friendly URL to profile page + */ + function getProfileLink() + { + return $this->basepath . '/' . $this->username; + } + /** * Check that the account has been registered and can be used. * On failure, throws a test failure exception. @@ -271,6 +375,7 @@ class SNTestClient extends TestBase $this->assertEqual($this->fullname, $data['name']); $this->assertEqual($this->homepage, $data['url']); $this->assertEqual($this->bio, $data['description']); + $this->log(" looks good!"); } /** @@ -307,11 +412,11 @@ class SNTestClient extends TestBase } $tries--; if ($tries) { - $this->log("Didn't see it yet, waiting $timeout seconds"); + $this->log(" didn't see it yet, waiting $timeout seconds"); sleep($timeout); } } - throw new Exception("Message $notice_uri not received by $this->username"); + throw new Exception(" message $notice_uri not received by $this->username"); } /** @@ -341,30 +446,88 @@ class SNTestClient extends TestBase } foreach ($entries as $entry) { if ($entry->id == $notice_uri) { - $this->log("found it $notice_uri"); + $this->log(" found it $notice_uri"); return true; } - //$this->log("nope... " . $entry->id); } return false; } + /** + * @param string $profile user page link or webfinger + */ + function subscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('follow ' . $profile); + } + + /** + * @param string $profile user page link or webfinger + */ + function unsubscribe($profile) + { + // This uses the command interface, since there's not currently + // a friendly Twit-API way to do a fresh remote subscription and + // the web form's a pain to use. + $this->post('leave ' . $profile); + } + /** * Check that this account is subscribed to the given profile. * @param string $profile_uri URI for the profile to check for + * @return boolean */ - function assertHasSubscription($profile_uri) + function hasSubscription($profile_uri) { - throw new Exception('tbi'); + $this->log("Checking if $this->username has a subscription to $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($me, $profile_uri); } /** * Check that this account is subscribed to by the given profile. * @param string $profile_uri URI for the profile to check for + * @return boolean */ - function assertHasSubscriber($profile_uri) + function hasSubscriber($profile_uri) { - throw new Exception('tbi'); + $this->log("Checking if $this->username is subscribed to by $profile_uri"); + + $me = $this->getProfileUri(); + return $this->checkSubscription($profile_uri, $me); + } + + protected function checkSubscription($subscriber, $subscribed) + { + // Using FOAF as the API methods for checking the social graph + // currently are unfriendly to remote profiles + $ns_foaf = 'http://xmlns.com/foaf/0.1/'; + $ns_sioc = 'http://rdfs.org/sioc/ns#'; + $ns_rdf = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + + $dom = $this->xml($this->username . '/foaf'); + $agents = $dom->getElementsByTagNameNS($ns_foaf, 'Agent'); + foreach ($agents as $agent) { + $agent_uri = $agent->getAttributeNS($ns_rdf, 'about'); + if ($agent_uri == $subscriber) { + $follows = $agent->getElementsByTagNameNS($ns_sioc, 'follows'); + foreach ($follows as $follow) { + $target = $follow->getAttributeNS($ns_rdf, 'resource'); + if ($target == ($subscribed . '#acct')) { + $this->log(" confirmed $subscriber subscribed to $subscribed"); + return true; + } + } + $this->log(" we found $subscriber but they don't follow $subscribed"); + return false; + } + } + $this->log(" can't find $subscriber in {$this->username}'s social graph."); + return false; } } diff --git a/scripts/fixup_deletions.php b/scripts/fixup_deletions.php new file mode 100755 index 0000000000..07ada7f9d9 --- /dev/null +++ b/scripts/fixup_deletions.php @@ -0,0 +1,166 @@ +#!/usr/bin/env php +. + */ + +define('INSTALLDIR', realpath(dirname(__FILE__) . '/..')); + +$longoptions = array('dry-run', 'start=', 'end='); + +$helptext = <<query($query); + + if ($profile->fetch()) { + return intval($profile->id); + } else { + die("Something went awry; could not look up max used profile_id."); + } +} + +/** + * Check for profiles in the given id range that are missing, presumed deleted. + * + * @param int $start beginning profile.id, inclusive + * @param int $end final profile.id, inclusive + * @return array of integer profile.ids + * @access private + */ +function get_missing_profiles($start, $end) +{ + $query = sprintf("SELECT id FROM profile WHERE id BETWEEN %d AND %d", + $start, $end); + + $profile = new Profile(); + $profile->query($query); + + $all = range($start, $end); + $known = array(); + while ($row = $profile->fetch()) { + $known[] = intval($profile->id); + } + unset($profile); + + $missing = array_diff($all, $known); + return $missing; +} + +/** + * Look for stray notices from this profile and, if present, kill them. + * + * @param int $profile_id + * @param bool $dry if true, we won't delete anything + */ +function cleanup_missing_profile($profile_id, $dry) +{ + $notice = new Notice(); + $notice->profile_id = $profile_id; + $notice->find(); + if ($notice->N == 0) { + return; + } + + $s = ($notice->N == 1) ? '' : 's'; + print "Deleted profile $profile_id has $notice->N stray notice$s:\n"; + + while ($notice->fetch()) { + print " notice $notice->id"; + if ($dry) { + print " (skipped; dry run)\n"; + } else { + $victim = clone($notice); + try { + $victim->delete(); + print " (deleted)\n"; + } catch (Exception $e) { + print " FAILED: "; + print $e->getMessage(); + print "\n"; + } + } + } +} + +$dry = have_option('dry-run'); + +$max_profile_id = get_max_profile_id(); +$chunk = 1000; + +if (have_option('start')) { + $begin = intval(get_option_value('start')); +} else { + $begin = 1; +} +if (have_option('end')) { + $final = min($max_profile_id, intval(get_option_value('end'))); +} else { + $final = $max_profile_id; +} + +if ($begin < 1) { + die("Silly human, you can't begin before profile number 1!\n"); +} +if ($final < $begin) { + die("Silly human, you can't end at $final if it's before $begin!\n"); +} + +// Identify missing profiles... +for ($start = $begin; $start <= $final; $start += $chunk) { + $end = min($start + $chunk - 1, $final); + + print "Checking for missing profiles between id $start and $end"; + if ($dry) { + print " (dry run)"; + } + print "...\n"; + $missing = get_missing_profiles($start, $end); + + foreach ($missing as $profile_id) { + cleanup_missing_profile($profile_id, $dry); + } +} + +echo "done.\n"; +