--- /dev/null
+# Translation of Tumblr Crosspostr plugin in French
+# This file is distributed under the same license as the Tumblr Crosspostr package.
+#
+# Translators:
+# ijulien <julien.ott@gmail.com>, 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: Tumblr Crosspostr\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2014-02-12 14:10-0700\n"
+"PO-Revision-Date: 2014-02-13 17:02+0000\n"
+"Last-Translator: ijulien <julien.ott@gmail.com>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/tumblr-crosspostr/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+msgid "Connect to Tumblr"
+msgstr "Se connecter à Tumblr"
+
+msgid ""
+"Almost done! Connect your blog to Tumblr to begin crossposting with Tumblr "
+"Crosspostr."
+msgstr "Vous avez presque complété l'installation! Connectez votre blog à Tumblr pour commencer à poster avec Tumblr Crosspostr."
+
+#, php-format
+msgid ""
+"Tumblr Crosspostr is provided as free software, but sadly grocery stores do "
+"not offer free food. If you like this plugin, please consider %1$s to its "
+"%2$s. ♥ Thank you!"
+msgstr "Tumblr Crosspostr est fourni gratuitement, mais comme tout se paye dans ce bas monde, vous pouvez effectuer ddes %1$s à ses %2$s. ♥ Mille mercis!"
+
+msgid "making a donation"
+msgstr "faire un don"
+
+msgid "houseless, jobless, nomadic developer"
+msgstr "sans domicile ni emploi, développeur nomade"
+
+msgid "You can automatically copy this post to your Tumblr blog:"
+msgstr "Vous pouvez automatiquement copier ce post vers votre blog Tumblr:"
+
+#, php-format
+msgid ""
+"Compose your post for WordPress as you normally would, with the appropriate "
+"%sPost Format%s."
+msgstr "Composez normalement votre post pour Wordpress avec les %sPost Format%s."
+
+#, php-format
+msgid ""
+"In %sthe Tumblr Crosspostr box%s, ensure the \"Send this post to Tumblr?\" "
+"option is set to \"Yes.\" (You can set it to \"No\" if you do not want to "
+"copy this post to Tumblr.)"
+msgstr "Dans %sthe Tumblr Crosspostr box%s, assurez vous que \"Envoyer ce post vers Tumblr?\" est défini à \"Oui\". (Vous pouvez le définir à \"Non\" si vous ne souhaitez pas copier ce post vers Tumblr.)"
+
+msgid ""
+"If you have more than one Tumblr, choose the one you want to send this post "
+"to from the \"Send to my Tumblr blog\" list."
+msgstr "Si vous avez plus d'un blog Tumblr, choisissez celui vers lequel vous souhaitez envoyer ce post depuis la liste \"Envoyer vers mon blog Tumblr\"."
+
+msgid ""
+"Optionally, enter any additional details specifically for Tumblr, such as "
+"the \"Content source\" field."
+msgstr "Optionnel, entrez les détails complémentaires spécifiques à Tumblr, comme le champ \"Source du contenu\"."
+
+msgid ""
+"When you are done, click \"Publish\" (or \"Save Draft\"), and Tumblr "
+"Crosspostr will send your post to the Tumblr blog you chose."
+msgstr "Lorsque vous avez terminé, cliquez sur \"Publier\" (ou \"Sauvegarder comme Brouillon\") et Tumblr Crosspostr enverra votre post au blog Tumblr de votre choix."
+
+msgid ""
+"Note that Tumblr does not allow you to change the post format after you have"
+" saved a copy of your post, so please be sure you choose the appropriate "
+"Post Format before you save your post."
+msgstr "Veuillez noter que Tumblr ne vous autorise pas à changer le format du post après avoir enregistré une copie de votre post. Soyez donc certain d'avoir fait le bon choix de format avant la sauvegarde."
+
+msgid "Crossposting to Tumblr"
+msgstr "Crosspost vers Tumblr"
+
+msgid "Tumblr Crosspostr:"
+msgstr "Tumblr Crosspostr:"
+
+msgid "Tumblr Crosspostr support forum"
+msgstr "Support de Tumblr Crosspostr"
+
+msgid "Donate to Tumblr Crosspostr"
+msgstr "Faire un don à Tumblr Crosspostr"
+
+msgid "Consumer key cannot be empty."
+msgstr "La clé \"Consumer key\" ne peut pas être vide."
+
+msgid "Consumer secret cannot be empty."
+msgstr "La clé \"Consumer secret\" ne peut pas être vide."
+
+msgid "Tumblr Crosspostr Settings"
+msgstr "Paramètres Tumblr Crosspostr"
+
+msgid "Tumblr Crosspostr"
+msgstr "Tumblr Crosspostr"
+
+msgid "Tumblrize Archives"
+msgstr "Archives Tumblrize"
+
+msgid ""
+"Tumblr Crossposter does not yet have a connection to Tumblr. Are you sure "
+"you connected Tumblr Crosspostr to your Tumblr account?"
+msgstr "Tumblr Crosspostr ne s'est pas encore connecté à Tumblr. Etes-vous sûr d'avoir connecté Tumblr Crosspostr à votre compte Tumblr?"
+
+msgid "Send this post to Tumblr?"
+msgstr "Envoyer ce post à Tumblr?"
+
+msgid ""
+"If this post is in a category that Tumblr Crosspostr excludes, this will be "
+"ignored."
+msgstr "Si ce post est dans une catégorie exclue par Tumblr Crosspostr, il sera ignoré."
+
+msgid "Yes"
+msgstr "Oui"
+
+msgid "No"
+msgstr "Non"
+
+msgid "Crosspost destination"
+msgstr "Destination du post"
+
+msgid "Send to my Tumblr blog titled"
+msgstr "Envoyer vers mon blog Tumblr intitulé"
+
+msgid "Tumblr meta fields"
+msgstr "Champs de meta Tumblr"
+
+msgid "Content source:"
+msgstr "Source du contenu:"
+
+msgid ""
+"Set the content source of this post on Tumblr by pasting the URL where you "
+"found the content you are blogging about."
+msgstr "Définir la source du contenu de ce post sur Tumblr en collant l'URL à laquelle vous avez trouvé le contenu duquel vous discutez."
+
+msgid ""
+"Leave this blank to set the content source URL of your Tumblr post to the "
+"permalink of this WordPress post."
+msgstr "Laissez ce champ vide pour définir la source de contenu sur Tumblr pointant vers le permalien de ce post Wordpress."
+
+msgid "//original-source.com/"
+msgstr "//source-originale.com/"
+
+msgid "Provide source attribution, if relevant."
+msgstr "Si nécéssaire, attribuez la source de cet article."
+
+msgid "You do not have sufficient permissions to access this page."
+msgstr "Vous n'avez pas les autorisations nécessaires pour accéder à cette page."
+
+msgid "Connection to Tumblr"
+msgstr "Connecter à Tumblr"
+
+msgid "Required settings to connect to Tumblr."
+msgstr "Paramètres requis pour se connecter à Tumblr."
+
+msgid "Tumblr API key/OAuth consumer key"
+msgstr "Clé API/OAuth consumer fournie par Tumblr"
+
+msgid "Paste your API key here"
+msgstr "Collez votre clé API içi"
+
+msgid "Your Tumblr API key is also called your consumer key."
+msgstr "Votre clé API Tumblr est également appelée \"consumer key\"."
+
+#, php-format
+msgid "If you need an API key, you can %s."
+msgstr "Si vous avez besoin d'une clé API, vous pouvez %s."
+
+msgid ""
+"Get an API key from Tumblr by registering your WordPress blog as a new "
+"Tumblr app."
+msgstr "Obtenez une clé API de tumblr en enregistrant votre blog WordPress en tant qu'application Tumblr."
+
+msgid "create one here"
+msgstr "en créer une içi"
+
+msgid "OAuth consumer secret"
+msgstr "OAuth consumer secret"
+
+msgid "Paste your consumer secret here"
+msgstr "Collez votre consumer secret içi"
+
+msgid ""
+"Your consumer secret is like your app password. Never share this with "
+"anyone."
+msgstr "Votre consumer secret est comme un mot de passé d'application. Ne le partagez avec personne."
+
+msgid "Connect to Tumblr:"
+msgstr "Connecter avec Tumblr:"
+
+msgid "Click here to connect to Tumblr"
+msgstr "Cliquez içi pour vous connecter à Tumblr"
+
+msgid "Connected to Tumblr!"
+msgstr "Connecté à Tumblr!"
+
+msgid "Crossposting Options"
+msgstr "Options de crosspost"
+
+msgid "Options for customizing crossposting behavior."
+msgstr "Options de comportement crosspost."
+
+msgid "Default Tumblr blog for crossposts"
+msgstr "Blog Tumblr par défaut pour les crossposts"
+
+msgid ""
+"Choose which Tumblr blog you want to send your posts to by default. This can"
+" be overriden on a per-post basis, too."
+msgstr "Choisissez vers quel blog Tumblr vous souhaitez envoyer vos posts apr défaut. Ces paramètres peuvent être définis pour chaque post également."
+
+msgid "Do not crosspost entries in these categories:"
+msgstr "Ne pas crossposter les entrées dans ces catégories:"
+
+msgid ""
+"Will cause posts in the specificied categories never to be crossposted to "
+"Tumblr. This is useful if, for instance, you are creating posts "
+"automatically using another plugin and wish to avoid a feedback loop of "
+"crossposting back and forth from one service to another."
+msgstr "Les posts catégorisés ainsi ne seront jamais postés sur Tumblr. Cette option est particulièrement utile si par exemple vous créez des posts automatiquement avec un autre plugin et souhaitez éviter une boucle sans fin postant d'un service à l'autre."
+
+msgid ""
+"Use permalinks from this blog as the \"Content source\" for crossposts on "
+"Tumblr?"
+msgstr "Utilisez des permaliens de ce blog comme \"Source de contenu\" pour les crossposts vers Tumblr?"
+
+#, php-format
+msgid ""
+"When enabled, leaving the %sContent source%s field blank on a given entry "
+"will result in setting %sthe \"Content source\" field on your Tumblr post%s "
+"to the permalink of your WordPress post. Useful for providing automatic "
+"back-links to your main blog, but turn this off if you \"secretly\" use "
+"Tumblr Crosspostr as the back-end of a publishing platform."
+msgstr "Si activé, laisser le champ %sContent source%s vide sur une entrée particulière sauvegardera le champ \"Content source\" de %s sur votre post Tumblr %s avec le permalien de votre post WordPress. Utile pour fournir des rétroliens automatiques vers votre blog principal, mais désactivez cette option si vous utilisez Tumblr Crosspostr comme back-end d'une plateforme de contenu."
+
+msgid "Add this markup to each crossposted entry."
+msgstr "Ajouter cette syntaxe à chaque entrée crosspostée."
+
+msgid "Anything you type in this box will be added to every crosspost."
+msgstr "Tout ce que vous ajoutez dans ce champ sera ajouté à chaque crosspost."
+
+msgid "Go to the original post."
+msgstr "Aller au post original."
+
+msgid "was originally published on"
+msgstr "initialement publié sur"
+
+msgid ""
+"Text or HTML you want to add to each post. Useful for things like a link "
+"back to your original post. You can use <code>%permalink%</code>, "
+"<code>%the_title%</code>, <code>%blog_url%</code>, and "
+"<code>%blog_name%</code> as placeholders for the cross-posted post's link, "
+"its title, the link to the homepage for this site, and the name of this "
+"blog, respectively. Leave this blank or use this field for a different "
+"purpose if you prefer to use only the Tumblr \"Content source\" meta field "
+"for links back to your main blog."
+msgstr "Texte ou HTML que vous souhaitez ajouter à chaque post. Utile par exemple pour fournir un lien vers votre post original. Vous pouvez utiliser <code>%permalink%</code>, <code>%the_title%</code>, <code>%blog_url%</code> et <code>%blog_name%</code> comme substituants pour le lien du post crossposté, le lien vers la page d'accueil de ce site et le nom de ce blog. Laissez ce champ vite ou utilisez-le à d'autres fins si vous préférez seulement utiliser le champ \"Source de contenu\" pour les liens vers votre blog."
+
+msgid "Do not send post tags to Tumblr"
+msgstr "Ne pas envoyer les tags des posts vers Tumblr"
+
+msgid ""
+"When enabled, tags on your WordPress posts are not applied to your Tumblr "
+"posts. Useful if you maintain different taxonomies on your different sites."
+msgstr "Si activé, les tags de vos posts Wordpress ne sont pas appliqués à vos posts Tumblr. Utile si vous maintenez deux systèmes de tags différents sur vos sites."
+
+msgid "Automatically add these tags to all crossposts:"
+msgstr "Ajouter automatiquement ces tags à tous les crossposts:"
+
+msgid "crosspost, magic"
+msgstr "crosspost, magique"
+
+#, php-format
+msgid ""
+"Comma-separated list of additional tags that will be added to every post "
+"sent to Tumblr. Useful if only some posts on your Tumblr blog are cross-"
+"posted and you want to know which of your Tumblr posts were generated by "
+"this plugin. (These tags will always be applied regardless of the value of "
+"the \"%s\" option.)"
+msgstr "Liste séparée par virgule de tags additionnels qui seront ajoutés à chaque post envoyé vers Tumblr. Utile si seulement certains de vos posts sur votre blog Tumblr sont crosspostés et vous souhaitez savoir si vos posts Tumblr ont été générés par ce plugin. (Ces tags seront toujours appliqués sans considération de la valeur de l'option \"%s\".)"
+
+#, php-format
+msgid "Success! %1$d post has been crossposted."
+msgstr "Bravo! %1$d a été crossposté."
+
+msgid "Blogs touched:"
+msgstr "Blogs concernés:"
+
+msgid "Crosspost Archives to Tumblr"
+msgstr "Crossposter les Archives vers Tumblr"
+
+msgid ""
+"If you have post archives on this website, Tumblr Crosspostr can copy them "
+"to your Tumblr blog."
+msgstr "Si vous avez des archives de posts sur ce site, Tumblr Crosspostr peut les copier vers votre blog Tumblr."
+
+#, php-format
+msgid ""
+"Copies all posts from your archives to your default Tumblr blog (%s). This "
+"may take some time if you have a lot of content. If you do not want to "
+"crosspost a specific post, set the answer to the \"Send this post to "
+"Tumblr?\" question to \"No\" when editing those posts before taking this "
+"action. If you have previously crossposted some posts, this will update that"
+" content on your Tumblr blog(s)."
+msgstr "Copie tous les posts de vos archives vers votre Tumblr blog par défaut (%s). Cette copie peut prendre du temps si vous avez beaucoup de contenu. Si vous ne souhaitez pas crossposter un post spécifique, désactivez l'option \"Envoyer ce post vers Tumblr?\" lorsque vous editez ces posts et avant de les sauvegarder. Si vous avez crosspostés certains posts auparavant, le contenu sera mis à jour sur vos blogs Tumblr."
--- /dev/null
+# Copyright (C) 2015 Tumblr Crosspostr
+# This file is distributed under the same license as the Tumblr Crosspostr package.
+msgid ""
+msgstr ""
+"Project-Id-Version: Tumblr Crosspostr 0.7.24\n"
+"Report-Msgid-Bugs-To: http://wordpress.org/tag/tumblr-crosspostr\n"
+"POT-Creation-Date: 2015-01-31 07:42:24+00:00\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"PO-Revision-Date: 2015-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+
+#: tumblr-crosspostr.php:110
+msgid "Connect to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:110
+msgid ""
+"Almost done! Connect your blog to Tumblr to begin crossposting with Tumblr "
+"Crosspostr."
+msgstr ""
+
+#: tumblr-crosspostr.php:136
+msgid ""
+"Tumblr Crosspostr is provided as free software, but sadly grocery stores do "
+"not offer free food. If you like this plugin, please consider %1$s to its "
+"%2$s. ♥ Thank you!"
+msgstr ""
+
+#: tumblr-crosspostr.php:137
+msgid "making a donation"
+msgstr ""
+
+#: tumblr-crosspostr.php:138
+msgid "houseless, jobless, nomadic developer"
+msgstr ""
+
+#: tumblr-crosspostr.php:198
+msgid "You can automatically copy this post to your Tumblr blog:"
+msgstr ""
+
+#: tumblr-crosspostr.php:201
+msgid ""
+"Compose your post for WordPress as you normally would, with the appropriate "
+"%sPost Format%s."
+msgstr ""
+
+#: tumblr-crosspostr.php:205
+msgid ""
+"In %sthe Tumblr Crosspostr box%s, ensure the \"Send this post to Tumblr?\" "
+"option is set to \"Yes.\" (You can set it to \"No\" if you do not want to "
+"copy this post to Tumblr.)"
+msgstr ""
+
+#: tumblr-crosspostr.php:208
+msgid ""
+"If you have more than one Tumblr, choose the one you want to send this post "
+"to from the \"Send to my Tumblr blog\" list."
+msgstr ""
+
+#: tumblr-crosspostr.php:209
+msgid ""
+"Optionally, enter any additional details specifically for Tumblr, such as "
+"the \"Content source\" field."
+msgstr ""
+
+#: tumblr-crosspostr.php:211
+msgid ""
+"When you are done, click \"Publish\" (or \"Save Draft\"), and Tumblr "
+"Crosspostr will send your post to the Tumblr blog you chose."
+msgstr ""
+
+#: tumblr-crosspostr.php:212
+msgid ""
+"Note that Tumblr does not allow you to change the post format after you have "
+"saved a copy of your post, so please be sure you choose the appropriate Post "
+"Format before you save your post."
+msgstr ""
+
+#: tumblr-crosspostr.php:220
+msgid "Crossposting to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:224
+msgid "Tumblr Crosspostr:"
+msgstr ""
+
+#: tumblr-crosspostr.php:225 tumblr-crosspostr.php:252
+msgid "Tumblr Crosspostr support forum"
+msgstr ""
+
+#: tumblr-crosspostr.php:226 tumblr-crosspostr.php:251
+msgid "Donate to Tumblr Crosspostr"
+msgstr ""
+
+#: tumblr-crosspostr.php:243 tumblr-crosspostr.php:553
+#: tumblr-crosspostr.php:930
+msgid "View post on Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:514
+msgid "Crossposting to Tumblr failed."
+msgstr ""
+
+#: tumblr-crosspostr.php:516
+msgid " Remote service said:"
+msgstr ""
+
+#: tumblr-crosspostr.php:518
+msgid "Response code:"
+msgstr ""
+
+#: tumblr-crosspostr.php:519
+msgid "Response message:"
+msgstr ""
+
+#: tumblr-crosspostr.php:526
+msgid ""
+"This might mean your %1$s are invalid or have been revoked by Tumblr. If "
+"everything looks fine on your end, you may want to ask %2$s to confirm your "
+"app is still allowed to use their API."
+msgstr ""
+
+#: tumblr-crosspostr.php:527
+msgid "OAuth credentials"
+msgstr ""
+
+#: tumblr-crosspostr.php:534
+msgid ""
+"Unfortunately, I have no idea what Tumblr is talking about. Consider asking "
+"%1$s for help. Tell them you are using %2$s, that you got the error shown "
+"above, and ask them to please support this tool. 'Cause, y'know, it's not "
+"like you don't already have a WordPress blog, and don't they want you to use "
+"Tumblr, too?"
+msgstr ""
+
+#. #-#-#-#-# tumblr-crosspostr.pot (Tumblr Crosspostr 0.7.24) #-#-#-#-#
+#. Plugin Name of the plugin/theme
+#: tumblr-crosspostr.php:536 tumblr-crosspostr.php:872
+#: tumblr-crosspostr.php:895
+msgid "Tumblr Crosspostr"
+msgstr ""
+
+#: tumblr-crosspostr.php:542
+msgid ""
+"Additionally, you may want to turn on Tumblr Crosspostr's \"%s\" option to "
+"get more information about this error the next time it happens."
+msgstr ""
+
+#: tumblr-crosspostr.php:544 tumblr-crosspostr.php:1279
+msgid "Enable detailed debugging information?"
+msgstr ""
+
+#: tumblr-crosspostr.php:553
+msgid "Post crossposted."
+msgstr ""
+
+#: tumblr-crosspostr.php:576
+msgid "Debug output:"
+msgstr ""
+
+#: tumblr-crosspostr.php:583
+msgid "Tumblr Support"
+msgstr ""
+
+#: tumblr-crosspostr.php:728
+msgid "Watch this video."
+msgstr ""
+
+#: tumblr-crosspostr.php:810
+msgid "Consumer key cannot be empty."
+msgstr ""
+
+#: tumblr-crosspostr.php:817
+msgid "Consumer secret cannot be empty."
+msgstr ""
+
+#: tumblr-crosspostr.php:871 tumblr-crosspostr.php:1020
+msgid "Tumblr Crosspostr Settings"
+msgstr ""
+
+#: tumblr-crosspostr.php:879 tumblr-crosspostr.php:880
+msgid "Tumblrize Archives"
+msgstr ""
+
+#: tumblr-crosspostr.php:915
+msgid ""
+"Tumblr Crossposter does not yet have a connection to Tumblr. Are you sure "
+"you connected Tumblr Crosspostr to your Tumblr account?"
+msgstr ""
+
+#: tumblr-crosspostr.php:936
+msgid "Send this post to Tumblr?"
+msgstr ""
+
+#: tumblr-crosspostr.php:937
+msgid ""
+"If this post is in a category that Tumblr Crosspostr excludes, this will be "
+"ignored."
+msgstr ""
+
+#: tumblr-crosspostr.php:939 tumblr-crosspostr.php:1133
+msgid "Yes"
+msgstr ""
+
+#: tumblr-crosspostr.php:940 tumblr-crosspostr.php:1142
+msgid "No"
+msgstr ""
+
+#: tumblr-crosspostr.php:944 tumblr-crosspostr.php:1024
+#: tumblr-crosspostr.php:1087
+msgid "Crossposting options"
+msgstr ""
+
+#: tumblr-crosspostr.php:946
+msgid "Destination & content"
+msgstr ""
+
+#: tumblr-crosspostr.php:948
+msgid "Send to my Tumblr blog titled"
+msgstr ""
+
+#: tumblr-crosspostr.php:952
+msgid "Send excerpt instead of main content?"
+msgstr ""
+
+#: tumblr-crosspostr.php:955
+msgid "Uncheck to send post content as crosspost content."
+msgstr ""
+
+#: tumblr-crosspostr.php:959
+msgid "Content source:"
+msgstr ""
+
+#: tumblr-crosspostr.php:962
+msgid ""
+"Set the content source of this post on Tumblr by pasting the URL where you "
+"found the content you are blogging about."
+msgstr ""
+
+#: tumblr-crosspostr.php:962
+msgid ""
+"Leave this blank to set the content source URL of your Tumblr post to the "
+"permalink of this WordPress post."
+msgstr ""
+
+#: tumblr-crosspostr.php:964
+msgid "//original-source.com/"
+msgstr ""
+
+#: tumblr-crosspostr.php:965
+msgid "Provide source attribution, if relevant."
+msgstr ""
+
+#: tumblr-crosspostr.php:972
+msgid "Social media broadcasts"
+msgstr ""
+
+#: tumblr-crosspostr.php:974
+msgid "Twitter"
+msgstr ""
+
+#: tumblr-crosspostr.php:977 tumblr-crosspostr.php:1227
+msgid "Send tweet?"
+msgstr ""
+
+#: tumblr-crosspostr.php:980
+msgid "Uncheck to disable the auto-tweet."
+msgstr ""
+
+#: tumblr-crosspostr.php:986
+msgid ""
+"If your Tumblr automatically tweets new posts to your Twitter account, you "
+"can customize the default tweet text by entering it here."
+msgstr ""
+
+#: tumblr-crosspostr.php:987
+msgid "New post: %s :)"
+msgstr ""
+
+#: tumblr-crosspostr.php:989
+msgid ""
+"Use %s where you want the link to your Tumblr post to appear, or leave blank "
+"to use the default Tumblr auto-tweet."
+msgstr ""
+
+#: tumblr-crosspostr.php:1004
+msgid "You do not have sufficient permissions to access this page."
+msgstr ""
+
+#: tumblr-crosspostr.php:1013
+msgid "Disconnected from Tumblr."
+msgstr ""
+
+#: tumblr-crosspostr.php:1014
+msgid ""
+"The connection to Tumblr was disestablished. You can reconnect using the "
+"same credentials, or enter different credentials before reconnecting."
+msgstr ""
+
+#: tumblr-crosspostr.php:1021
+msgid "Jump to options:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1023 tumblr-crosspostr.php:1030
+msgid "Connection to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:1025 tumblr-crosspostr.php:1233
+msgid "Sync options"
+msgstr ""
+
+#: tumblr-crosspostr.php:1026 tumblr-crosspostr.php:1273
+msgid "Plugin extras"
+msgstr ""
+
+#: tumblr-crosspostr.php:1031
+msgid "Required settings to connect to Tumblr."
+msgstr ""
+
+#: tumblr-crosspostr.php:1035
+msgid "Tumblr API key/OAuth consumer key"
+msgstr ""
+
+#: tumblr-crosspostr.php:1038
+msgid "Paste your API key here"
+msgstr ""
+
+#: tumblr-crosspostr.php:1040
+msgid "Your Tumblr API key is also called your consumer key."
+msgstr ""
+
+#: tumblr-crosspostr.php:1042
+msgid "If you need an API key, you can %s."
+msgstr ""
+
+#: tumblr-crosspostr.php:1044
+msgid ""
+"Get an API key from Tumblr by registering your WordPress blog as a new "
+"Tumblr app."
+msgstr ""
+
+#: tumblr-crosspostr.php:1045
+msgid "create one here"
+msgstr ""
+
+#: tumblr-crosspostr.php:1052
+msgid "OAuth consumer secret"
+msgstr ""
+
+#: tumblr-crosspostr.php:1055
+msgid "Paste your consumer secret here"
+msgstr ""
+
+#: tumblr-crosspostr.php:1057
+msgid ""
+"Your consumer secret is like your app password. Never share this with anyone."
+msgstr ""
+
+#: tumblr-crosspostr.php:1064
+msgid "Connect to Tumblr:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1067
+msgid "Click here to connect to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:1075
+msgid "Connected to Tumblr!"
+msgstr ""
+
+#: tumblr-crosspostr.php:1076
+msgid "Disconnect"
+msgstr ""
+
+#: tumblr-crosspostr.php:1077
+msgid ""
+"Disconnecting will stop cross-posts from appearing on or being imported from "
+"your Tumblr blog(s), and will reset the options below to their defaults. You "
+"can re-connect at any time."
+msgstr ""
+
+#: tumblr-crosspostr.php:1088
+msgid "Options for customizing crossposting behavior."
+msgstr ""
+
+#: tumblr-crosspostr.php:1092
+msgid "Default Tumblr blog for crossposts"
+msgstr ""
+
+#: tumblr-crosspostr.php:1096
+msgid ""
+"Choose which Tumblr blog you want to send your posts to by default. This can "
+"be overriden on a per-post basis, too."
+msgstr ""
+
+#: tumblr-crosspostr.php:1101
+msgid "Do not crosspost entries in these categories:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1118
+msgid ""
+"Will cause posts in the specificied categories never to be crossposted to "
+"Tumblr. This is useful if, for instance, you are creating posts "
+"automatically using another plugin and wish to avoid a feedback loop of "
+"crossposting back and forth from one service to another."
+msgstr ""
+
+#: tumblr-crosspostr.php:1123
+msgid ""
+"Use permalinks from this blog as the \"Content source\" for crossposts on "
+"Tumblr?"
+msgstr ""
+
+#: tumblr-crosspostr.php:1146
+msgid ""
+"When enabled, leaving the %sContent source%s field blank on a given entry "
+"will result in setting %sthe \"Content source\" field on your Tumblr post%s "
+"to the permalink of your WordPress post. Useful for providing automatic back-"
+"links to your main blog, but turn this off if you \"secretly\" use Tumblr "
+"Crosspostr as the back-end of a publishing platform."
+msgstr ""
+
+#: tumblr-crosspostr.php:1151
+msgid "Send excerpts instead of main content?"
+msgstr ""
+
+#: tumblr-crosspostr.php:1155
+msgid ""
+"When enabled, the excerpts (as opposed to the body) of your WordPress posts "
+"will be used as the main content of your Tumblr posts. Useful if you prefer "
+"to crosspost summaries instead of the full text of your entires to Tumblr by "
+"default. This can be overriden on a per-post basis, too."
+msgstr ""
+
+#: tumblr-crosspostr.php:1160
+msgid "Crosspost the following post types:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1178
+msgid ""
+"Choose which %1$spost types%2$s you want to crosspost. Not all post types "
+"can be crossposted safely, but many can. If you are not sure about a post "
+"type, leave it disabled. Plugin authors may create post types that are "
+"crossposted regardless of the value of this setting. %3$spost%4$s post types "
+"are always enabled."
+msgstr ""
+
+#: tumblr-crosspostr.php:1183
+msgid "Add the following markup to each crossposted entry:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1189
+msgid "Anything you type in this box will be added to every crosspost."
+msgstr ""
+
+#: tumblr-crosspostr.php:1193
+msgid "Go to the original post."
+msgstr ""
+
+#: tumblr-crosspostr.php:1193
+msgid "was originally published on"
+msgstr ""
+
+#: tumblr-crosspostr.php:1196
+msgid ""
+"Text or HTML you want to add to each post. Useful for things like a link "
+"back to your original post. You can use <code>%permalink%</code>, <code>"
+"%the_title%</code>, <code>%blog_url%</code>, and <code>%blog_name%</code> as "
+"placeholders for the cross-posted post's link, its title, the link to the "
+"homepage for this site, and the name of this blog, respectively. Leave this "
+"blank or use this field for a different purpose if you prefer to use only "
+"the Tumblr \"Content source\" meta field for links back to your main blog."
+msgstr ""
+
+#: tumblr-crosspostr.php:1201 tumblr-crosspostr.php:1216
+msgid "Do not send post tags to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:1205
+msgid ""
+"When enabled, tags on your WordPress posts are not applied to your Tumblr "
+"posts. Useful if you maintain different taxonomies on your different sites."
+msgstr ""
+
+#: tumblr-crosspostr.php:1211
+msgid "Automatically add these tags to all crossposts:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1215
+msgid "crosspost, magic"
+msgstr ""
+
+#: tumblr-crosspostr.php:1216
+msgid ""
+"Comma-separated list of additional tags that will be added to every post "
+"sent to Tumblr. Useful if only some posts on your Tumblr blog are cross-"
+"posted and you want to know which of your Tumblr posts were generated by "
+"this plugin. (These tags will always be applied regardless of the value of "
+"the \"%s\" option.)"
+msgstr ""
+
+#: tumblr-crosspostr.php:1222
+msgid "Automatically tweet a link to your Tumblr post?"
+msgstr ""
+
+#: tumblr-crosspostr.php:1227
+msgid ""
+"When checked, new posts you create on WordPress will have their \"%s\" "
+"option enabled by default. You can always override this when editing an "
+"individual post."
+msgstr ""
+
+#: tumblr-crosspostr.php:1234
+msgid "Customize the import behavior."
+msgstr ""
+
+#: tumblr-crosspostr.php:1238
+msgid "Sync posts from Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:1239
+msgid ""
+"(This feature is experimental. Please backup your website before you turn "
+"this on.)"
+msgstr ""
+
+#: tumblr-crosspostr.php:1245
+msgid ""
+"Content you create on the Tumblr blogs you select will automatically be "
+"copied to this blog."
+msgstr ""
+
+#: tumblr-crosspostr.php:1250
+msgid "Automatically assign synced posts to these categories:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1267
+msgid ""
+"Will cause any posts imported from your Tumblr blog to be assigned the "
+"categories that you enable here. It is often a good idea to %screate a new "
+"category%s that you use exclusively for this purpose."
+msgstr ""
+
+#: tumblr-crosspostr.php:1274
+msgid "Additional options to customize plugin behavior."
+msgstr ""
+
+#: tumblr-crosspostr.php:1286
+msgid ""
+"Turn this on only if you are experiencing problems using this plugin, or if "
+"you were told to do so by someone helping you fix a problem (or if you "
+"really know what you are doing). When enabled, extremely detailed technical "
+"information is displayed as a WordPress admin notice when you take actions "
+"like sending a crosspost. If you have also enabled WordPress's built-in "
+"debugging (%1$s) and debug log (%2$s) feature, additional information will "
+"be sent to a log file (%3$s). This file may contain sensitive information, "
+"so turn this off and erase the debug log file when you have resolved the "
+"issue."
+msgstr ""
+
+#: tumblr-crosspostr.php:1331
+msgid "Success! %1$d post has been crossposted."
+msgid_plural "Success! %1$d posts have been crossposted to %2$d blogs."
+msgstr[0] ""
+msgstr[1] ""
+
+#: tumblr-crosspostr.php:1340
+msgid "Blogs touched:"
+msgstr ""
+
+#: tumblr-crosspostr.php:1353
+msgid "Crosspost Archives to Tumblr"
+msgstr ""
+
+#: tumblr-crosspostr.php:1354
+msgid ""
+"If you have post archives on this website, Tumblr Crosspostr can copy them "
+"to your Tumblr blog."
+msgstr ""
+
+#: tumblr-crosspostr.php:1356
+msgid ""
+"Copies all posts from your archives to your default Tumblr blog (%s). This "
+"may take some time if you have a lot of content. If you do not want to "
+"crosspost a specific post, set the answer to the \"Send this post to Tumblr?"
+"\" question to \"No\" when editing those posts before taking this action. If "
+"you have previously crossposted some posts, this will update that content on "
+"your Tumblr blog(s)."
+msgstr ""
+
+#: tumblr-crosspostr.php:1436
+msgid "Entering Tumblr Sync routine for %s"
+msgstr ""
+
+#: tumblr-crosspostr.php:1460
+msgid "Found %s preexisting post for Tumblr ID"
+msgid_plural "Found %s preexisting posts for Tumblr ID"
+msgstr[0] ""
+msgstr[1] ""
+
+#: tumblr-crosspostr.php:1475
+msgid "Previously synced, stopping."
+msgstr ""
+
+#: tumblr-crosspostr.php:1572
+msgid ""
+"Your WordPress uploads directory (%s) is not writeable, so Tumblr Crosspostr "
+"could not import some media files directly into your Media Library. Media "
+"(such as images) will be referenced from their remote source rather than "
+"imported and referenced locally."
+msgstr ""
+
+#: tumblr-crosspostr.php:1609
+msgid ""
+"Failed to get Tumblr media (%1$s) from post (%2$s). Server responded: %3$s"
+msgstr ""
+
+#: tumblr-crosspostr.php:1619
+msgid "Error saving file (%s): "
+msgstr ""
+
+#. Plugin URI of the plugin/theme
+msgid "https://github.com/meitar/tumblr-crosspostr/#readme"
+msgstr ""
+
+#. Description of the plugin/theme
+msgid ""
+"Automatically crossposts to your Tumblr blog when you publish a post on your "
+"WordPress blog."
+msgstr ""
+
+#. Author of the plugin/theme
+msgid "Meitar Moscovitz"
+msgstr ""
+
+#. Author URI of the plugin/theme
+msgid "http://Cyberbusking.org/"
+msgstr ""
--- /dev/null
+<?php
+/**
+ * OAuthWP is a base class useful for creating WordPress plugins that
+ * use any version of the OAuth protocol. It has two primary and very
+ * simple classes: OAuthWP and Plugin_OAuthWP.
+ *
+ * OAuthWP extends Manuel Lemos's oauth_client_class to support each
+ * version of the OAuth protocol (1, 1.0a, and 2.0).
+ *
+ * Plugin_OAuthWP is an abstract class that provides a skeleton for
+ * common methods needed by WordPress plugins that use OAuth.
+ * (For example, its $client property is an instance of OAuthWP.)
+ */
+if (!class_exists('http_class')) { require_once 'httpclient/http.php'; }
+if (!class_exists('oauth_client_class')) { require_once 'oauth_api/oauth_client.php'; }
+// Wrapper for OAuth support for WordPress plugins.
+// Check if class exists in case it was included by another plugin.
+if (!class_exists('OAuthWP')) :
+abstract class OAuthWP extends oauth_client_class {
+
+ public function authorize ($redirect_uri) {
+ $this->redirect_uri = $redirect_uri;
+ $s = $this->Process();
+ if ($this->exit) {
+ $this->Finalize($s);
+ exit();
+ }
+ }
+
+ public function completeAuthorization ($redirect_uri) {
+ $this->redirect_uri = $redirect_uri;
+ $this->Process();
+ $tokens = array();
+ $this->GetAccessToken($tokens);
+ return $tokens;
+ }
+
+}
+endif; // !class_exists('OAuthWP')
+
+// Define a class that uses the above.
+if (!class_exists('Plugin_OAuthWP')) :
+abstract class Plugin_OAuthWP {
+ public $client; //< OAuth consumer, should be a child of OAuthWP.
+
+ abstract public function __construct ($consumer_key = '', $consumer_secret = '');
+
+ abstract public function getAppRegistrationUrl ($params = array());
+
+ protected function appRegistrationUrl ($base_url, $params = array()) {
+ if (empty($base_url)) {
+ throw new Exception('Empty base_url.');
+ }
+ $url = $base_url . '?';
+ $i = 0;
+ foreach ($params as $k => $v) {
+ if (0 !== $i) { $url .= '&'; }
+ $url .= $k . '=' . urlencode($v);
+ $i++;
+ }
+ return $url;
+ }
+
+ public function authorize ($redirect_uri) {
+ $this->client->authorize($redirect_uri);
+ }
+
+ public function completeAuthorization ($redirect_uri) {
+ return $this->client->completeAuthorization($redirect_uri);
+ }
+
+ protected function talkToService ($path, $params = array(), $method = 'POST', $opts = array()) {
+ $resp = null;
+ if ($s = @$this->client->CallAPI($path, $method, $params, $opts, $resp)) {
+ $this->client->Finalize($s);
+ }
+ return $resp;
+ }
+
+}
+endif; // !class_exists('Plugin_OAuthWP')
--- /dev/null
+<?php
+require_once 'OAuthWP.php';
+
+class OAuthWP_Tumblr extends OAuthWP {
+
+ // Override so clients can ignore the API base url.
+ public function CallAPI ($url, $method, $params, $opts, &$resp) {
+ return parent::CallAPI('http://api.tumblr.com/v2' . $url, $method, $params, $opts, $resp);
+ }
+
+}
+
+abstract class Tumblr_OAuthWP_Plugin extends Plugin_OAuthWP {
+
+ public function getAppRegistrationUrl ($params = array()) {
+ $x = array(
+ 'oac[title]' => $params['title'],
+ 'oac[description]' => $params['description'],
+ 'oac[url]' => $params['url'],
+ 'oac[admin_contact_email]' => $params['admin_contact_email'],
+ 'oac[default_callback_url]' => $params['default_callback_url']
+ );
+ return $this->appRegistrationUrl('http://www.tumblr.com/oauth/register', $x);
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * Super-skeletal class to interact with Tumblr from Tumblr Crosspostr plugin.
+ */
+
+// Loads OAuth consumer class via OAuthWP class.
+require_once 'OAuthWP_Tumblr.php';
+
+class Tumblr_Crosspostr_API_Client extends Tumblr_OAuthWP_Plugin {
+ private $api_key; //< Also the "Consumer key" the user entered.
+
+ function __construct ($consumer_key = '', $consumer_secret = '') {
+ $this->client = new OAuthWP_Tumblr;
+ $this->client->server = 'Tumblr';
+ $this->client->client_id = $consumer_key;
+ $this->client->client_secret = $consumer_secret;
+ $this->client->configuration_file = dirname(__FILE__) . '/oauth_api/oauth_configuration.json';
+ $this->client->Initialize();
+
+ return $this;
+ }
+
+ // Needed for some GET requests.
+ public function setApiKey ($key) {
+ $this->api_key = $key;
+ }
+
+ public function getUserBlogs () {
+ $data = $this->talkToService('/user/info', array(), 'GET');
+ // TODO: This could use some error handling?
+ if (isset($data)) {
+ return $data->response->user->blogs;
+ } else {
+ return false;
+ }
+ }
+
+ public function getBlogInfo ($base_hostname) {
+ $data = $this->talkToService("/blog/$base_hostname/info?api_key={$this->api_key}", array(), 'GET');
+ // TODO: Handle error?
+ return $data->response->blog;
+ }
+
+ public function getPosts ($base_hostname, $params = array()) {
+ $url = "/blog/$base_hostname/posts?api_key={$this->api_key}";
+ if (!empty($params)) {
+ foreach ($params as $k => $v) {
+ $url .= "&$k=$v";
+ }
+ }
+ $data = $this->talkToService($url, array(), 'GET');
+ return $data->response;
+ }
+
+ public function postToTumblrBlog ($blog, $params) {
+ $api_method = "/blog/$blog/post";
+ return $this->talkToService($api_method, $params);
+ }
+ public function editOnTumblrBlog ($blog, $params) {
+ $api_method = "/blog/$blog/post/edit";
+ return $this->talkToService($api_method, $params);
+ }
+ public function deleteFromTumblrBlog ($blog, $params) {
+ $api_method = "/blog/$blog/post/delete";
+ return $this->talkToService($api_method, $params);
+ }
+
+}
--- /dev/null
+HTTP client PHP class
+
+This LICENSE is in the BSD license style.
+
+License Version Control:
+@(#) $Id: LICENSE.txt,v 1.1 2006/04/17 19:44:04 mlemos Exp $
+
+Copyright (c) 1999 - 2006, Manuel Lemos
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ Neither the name of Manuel Lemos nor the names of his contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+<?php
+/*
+ * http.php
+ *
+ * @(#) $Header: /opt2/ena/metal/http/http.php,v 1.90 2013/02/20 11:45:28 mlemos Exp $
+ *
+ */
+
+define('HTTP_CLIENT_ERROR_UNSPECIFIED_ERROR', -1);
+define('HTTP_CLIENT_ERROR_NO_ERROR', 0);
+define('HTTP_CLIENT_ERROR_INVALID_SERVER_ADDRESS', 1);
+define('HTTP_CLIENT_ERROR_CANNOT_CONNECT', 2);
+define('HTTP_CLIENT_ERROR_COMMUNICATION_FAILURE', 3);
+define('HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE', 4);
+define('HTTP_CLIENT_ERROR_PROTOCOL_FAILURE', 5);
+define('HTTP_CLIENT_ERROR_INVALID_PARAMETERS', 6);
+
+class http_class
+{
+ var $host_name="";
+ var $host_port=0;
+ var $proxy_host_name="";
+ var $proxy_host_port=80;
+ var $socks_host_name = '';
+ var $socks_host_port = 1080;
+ var $socks_version = '5';
+
+ var $protocol="http";
+ var $request_method="GET";
+ var $user_agent='httpclient (http://www.phpclasses.org/httpclient $Revision: 1.90 $)';
+ var $accept='';
+ var $authentication_mechanism="";
+ var $user;
+ var $password;
+ var $realm;
+ var $workstation;
+ var $proxy_authentication_mechanism="";
+ var $proxy_user;
+ var $proxy_password;
+ var $proxy_realm;
+ var $proxy_workstation;
+ var $request_uri="";
+ var $request="";
+ var $request_headers=array();
+ var $request_user;
+ var $request_password;
+ var $request_realm;
+ var $request_workstation;
+ var $proxy_request_user;
+ var $proxy_request_password;
+ var $proxy_request_realm;
+ var $proxy_request_workstation;
+ var $request_body="";
+ var $request_arguments=array();
+ var $protocol_version="1.1";
+ var $timeout=0;
+ var $data_timeout=0;
+ var $debug=0;
+ var $log_debug=0;
+ var $debug_response_body=1;
+ var $html_debug=0;
+ var $support_cookies=1;
+ var $cookies=array();
+ var $error="";
+ var $error_code = HTTP_CLIENT_ERROR_NO_ERROR;
+ var $exclude_address="";
+ var $follow_redirect=0;
+ var $redirection_limit=5;
+ var $response_status="";
+ var $response_message="";
+ var $file_buffer_length=8000;
+ var $force_multipart_form_post=0;
+ var $prefer_curl = 0;
+ var $keep_alive = 1;
+ var $sasl_authenticate = 1;
+
+ /* private variables - DO NOT ACCESS */
+
+ var $state="Disconnected";
+ var $use_curl=0;
+ var $connection=0;
+ var $content_length=0;
+ var $response="";
+ var $read_response=0;
+ var $read_length=0;
+ var $request_host="";
+ var $next_token="";
+ var $redirection_level=0;
+ var $chunked=0;
+ var $remaining_chunk=0;
+ var $last_chunk_read=0;
+ var $months=array(
+ "Jan"=>"01",
+ "Feb"=>"02",
+ "Mar"=>"03",
+ "Apr"=>"04",
+ "May"=>"05",
+ "Jun"=>"06",
+ "Jul"=>"07",
+ "Aug"=>"08",
+ "Sep"=>"09",
+ "Oct"=>"10",
+ "Nov"=>"11",
+ "Dec"=>"12");
+ var $session='';
+ var $connection_close=0;
+ var $force_close = 0;
+ var $connected_host = '';
+ var $connected_port = -1;
+ var $connected_ssl = 0;
+
+ /* Private methods - DO NOT CALL */
+
+ Function Tokenize($string,$separator="")
+ {
+ if(!strcmp($separator,""))
+ {
+ $separator=$string;
+ $string=$this->next_token;
+ }
+ for($character=0;$character<strlen($separator);$character++)
+ {
+ if(GetType($position=strpos($string,$separator[$character]))=="integer")
+ $found=(IsSet($found) ? min($found,$position) : $position);
+ }
+ if(IsSet($found))
+ {
+ $this->next_token=substr($string,$found+1);
+ return(substr($string,0,$found));
+ }
+ else
+ {
+ $this->next_token="";
+ return($string);
+ }
+ }
+
+ Function CookieEncode($value, $name)
+ {
+ return($name ? str_replace("=", "%25", $value) : str_replace(";", "%3B", $value));
+ }
+
+ Function SetError($error, $error_code = HTTP_CLIENT_ERROR_UNSPECIFIED_ERROR)
+ {
+ $this->error_code = $error_code;
+ return($this->error=$error);
+ }
+
+ Function SetPHPError($error, &$php_error_message, $error_code = HTTP_CLIENT_ERROR_UNSPECIFIED_ERROR)
+ {
+ if(IsSet($php_error_message)
+ && strlen($php_error_message))
+ $error.=": ".$php_error_message;
+ return($this->SetError($error, $error_code));
+ }
+
+ Function SetDataAccessError($error,$check_connection=0)
+ {
+ $this->error=$error;
+ $this->error_code = HTTP_CLIENT_ERROR_COMMUNICATION_FAILURE;
+ if(!$this->use_curl
+ && function_exists("socket_get_status"))
+ {
+ $status=socket_get_status($this->connection);
+ if($status["timed_out"])
+ $this->error.=": data access time out";
+ elseif($status["eof"])
+ {
+ if($check_connection)
+ $this->error="";
+ else
+ $this->error.=": the server disconnected";
+ }
+ }
+ }
+
+ Function OutputDebug($message)
+ {
+ if($this->log_debug)
+ error_log($message);
+ else
+ {
+ $message.="\n";
+ if($this->html_debug)
+ $message=str_replace("\n","<br />\n",HtmlEntities($message));
+ echo $message;
+ flush();
+ }
+ }
+
+ Function GetLine()
+ {
+ for($line="";;)
+ {
+ if($this->use_curl)
+ {
+ $eol=strpos($this->response,"\n",$this->read_response);
+ $data=($eol ? substr($this->response,$this->read_response,$eol+1-$this->read_response) : "");
+ $this->read_response+=strlen($data);
+ }
+ else
+ {
+ if(feof($this->connection))
+ {
+ $this->SetDataAccessError("reached the end of data while reading from the HTTP server connection");
+ return(0);
+ }
+ $data=fgets($this->connection,100);
+ }
+ if(GetType($data)!="string"
+ || strlen($data)==0)
+ {
+ $this->SetDataAccessError("it was not possible to read line from the HTTP server");
+ return(0);
+ }
+ $line.=$data;
+ $length=strlen($line);
+ if($length
+ && !strcmp(substr($line,$length-1,1),"\n"))
+ {
+ $length-=(($length>=2 && !strcmp(substr($line,$length-2,1),"\r")) ? 2 : 1);
+ $line=substr($line,0,$length);
+ if($this->debug)
+ $this->OutputDebug("S $line");
+ return($line);
+ }
+ }
+ }
+
+ Function PutLine($line)
+ {
+ if($this->debug)
+ $this->OutputDebug("C $line");
+ if(!fputs($this->connection,$line."\r\n"))
+ {
+ $this->SetDataAccessError("it was not possible to send a line to the HTTP server");
+ return(0);
+ }
+ return(1);
+ }
+
+ Function PutData($data)
+ {
+ if(strlen($data))
+ {
+ if($this->debug)
+ $this->OutputDebug('C '.$data);
+ if(!fputs($this->connection,$data))
+ {
+ $this->SetDataAccessError("it was not possible to send data to the HTTP server");
+ return(0);
+ }
+ }
+ return(1);
+ }
+
+ Function FlushData()
+ {
+ if(!fflush($this->connection))
+ {
+ $this->SetDataAccessError("it was not possible to send data to the HTTP server");
+ return(0);
+ }
+ return(1);
+ }
+
+ Function ReadChunkSize()
+ {
+ if($this->remaining_chunk==0)
+ {
+ $debug=$this->debug;
+ if(!$this->debug_response_body)
+ $this->debug=0;
+ $line=$this->GetLine();
+ $this->debug=$debug;
+ if(GetType($line)!="string")
+ return($this->SetError("could not read chunk start: ".$this->error, $this->error_code));
+ $this->remaining_chunk=hexdec($line);
+ if($this->remaining_chunk == 0)
+ {
+ if(!$this->debug_response_body)
+ $this->debug=0;
+ $line=$this->GetLine();
+ $this->debug=$debug;
+ if(GetType($line)!="string")
+ return($this->SetError("could not read chunk end: ".$this->error, $this->error_code));
+ }
+ }
+ return("");
+ }
+
+ Function ReadBytes($length)
+ {
+ if($this->use_curl)
+ {
+ $bytes=substr($this->response,$this->read_response,min($length,strlen($this->response)-$this->read_response));
+ $this->read_response+=strlen($bytes);
+ if($this->debug
+ && $this->debug_response_body
+ && strlen($bytes))
+ $this->OutputDebug("S ".$bytes);
+ }
+ else
+ {
+ if($this->chunked)
+ {
+ for($bytes="",$remaining=$length;$remaining;)
+ {
+ if(strlen($this->ReadChunkSize()))
+ return("");
+ if($this->remaining_chunk==0)
+ {
+ $this->last_chunk_read=1;
+ break;
+ }
+ $ask=min($this->remaining_chunk,$remaining);
+ $chunk=@fread($this->connection,$ask);
+ $read=strlen($chunk);
+ if($read==0)
+ {
+ $this->SetDataAccessError("it was not possible to read data chunk from the HTTP server");
+ return("");
+ }
+ if($this->debug
+ && $this->debug_response_body)
+ $this->OutputDebug("S ".$chunk);
+ $bytes.=$chunk;
+ $this->remaining_chunk-=$read;
+ $remaining-=$read;
+ if($this->remaining_chunk==0)
+ {
+ if(feof($this->connection))
+ return($this->SetError("reached the end of data while reading the end of data chunk mark from the HTTP server", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ $data=@fread($this->connection,2);
+ if(strcmp($data,"\r\n"))
+ {
+ $this->SetDataAccessError("it was not possible to read end of data chunk from the HTTP server");
+ return("");
+ }
+ }
+ }
+ }
+ else
+ {
+ $bytes=@fread($this->connection,$length);
+ if(strlen($bytes))
+ {
+ if($this->debug
+ && $this->debug_response_body)
+ $this->OutputDebug("S ".$bytes);
+ }
+ else
+ $this->SetDataAccessError("it was not possible to read data from the HTTP server", $this->connection_close);
+ }
+ }
+ return($bytes);
+ }
+
+ Function EndOfInput()
+ {
+ if($this->use_curl)
+ return($this->read_response>=strlen($this->response));
+ if($this->chunked)
+ return($this->last_chunk_read);
+ if($this->content_length_set)
+ return($this->content_length <= $this->read_length);
+ return(feof($this->connection));
+ }
+
+ Function Resolve($domain, &$ip, $server_type)
+ {
+ if(preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/',$domain))
+ $ip=$domain;
+ else
+ {
+ if($this->debug)
+ $this->OutputDebug('Resolving '.$server_type.' server domain "'.$domain.'"...');
+ if(!strcmp($ip=@gethostbyname($domain),$domain))
+ $ip="";
+ }
+ if(strlen($ip)==0
+ || (strlen($this->exclude_address)
+ && !strcmp(@gethostbyname($this->exclude_address),$ip)))
+ return($this->SetError("could not resolve the host domain \"".$domain."\"", HTTP_CLIENT_ERROR_INVALID_SERVER_ADDRESS));
+ return('');
+ }
+
+ Function Connect($host_name, $host_port, $ssl, $server_type = 'HTTP')
+ {
+ $domain=$host_name;
+ $port = $host_port;
+ if(strlen($error = $this->Resolve($domain, $ip, $server_type)))
+ return($error);
+ if(strlen($this->socks_host_name))
+ {
+ switch($this->socks_version)
+ {
+ case '4':
+ $version = 4;
+ break;
+ case '5':
+ $version = 5;
+ break;
+ default:
+ return('it was not specified a supported SOCKS protocol version');
+ break;
+ }
+ $host_ip = $ip;
+ $port = $this->socks_host_port;
+ $host_server_type = $server_type;
+ $server_type = 'SOCKS';
+ if(strlen($error = $this->Resolve($this->socks_host_name, $ip, $server_type)))
+ return($error);
+ }
+ if($this->debug)
+ $this->OutputDebug('Connecting to '.$server_type.' server IP '.$ip.' port '.$port.'...');
+ if($ssl)
+ $ip="ssl://".$host_name;
+ if(($this->connection=($this->timeout ? @fsockopen($ip, $port, $errno, $error, $this->timeout) : @fsockopen($ip, $port, $errno)))==0)
+ {
+ $error_code = HTTP_CLIENT_ERROR_CANNOT_CONNECT;
+ switch($errno)
+ {
+ case -3:
+ return($this->SetError("socket could not be created", $error_code));
+ case -4:
+ return($this->SetError("dns lookup on hostname \"".$host_name."\" failed", $error_code));
+ case -5:
+ return($this->SetError("connection refused or timed out", $error_code));
+ case -6:
+ return($this->SetError("fdopen() call failed", $error_code));
+ case -7:
+ return($this->SetError("setvbuf() call failed", $error_code));
+ default:
+ return($this->SetPHPError($errno." could not connect to the host \"".$host_name."\"",$php_errormsg, $error_code));
+ }
+ }
+ else
+ {
+ if($this->data_timeout
+ && function_exists("socket_set_timeout"))
+ socket_set_timeout($this->connection,$this->data_timeout,0);
+ if(strlen($this->socks_host_name))
+ {
+ if($this->debug)
+ $this->OutputDebug('Connected to the SOCKS server '.$this->socks_host_name);
+ $send_error = 'it was not possible to send data to the SOCKS server';
+ $receive_error = 'it was not possible to receive data from the SOCKS server';
+ switch($version)
+ {
+ case 4:
+ $command = 1;
+ $user = '';
+ if(!fputs($this->connection, chr($version).chr($command).pack('nN', $host_port, ip2long($host_ip)).$user.Chr(0)))
+ $error = $this->SetDataAccessError($send_error);
+ else
+ {
+ $response = fgets($this->connection, 9);
+ if(strlen($response) != 8)
+ $error = $this->SetDataAccessError($receive_error);
+ else
+ {
+ $socks_errors = array(
+ "\x5a"=>'',
+ "\x5b"=>'request rejected',
+ "\x5c"=>'request failed because client is not running identd (or not reachable from the server)',
+ "\x5d"=>'request failed because client\'s identd could not confirm the user ID string in the request',
+ );
+ $error_code = $response[1];
+ $error = (IsSet($socks_errors[$error_code]) ? $socks_errors[$error_code] : 'unknown');
+ if(strlen($error))
+ $error = 'SOCKS error: '.$error;
+ }
+ }
+ break;
+ case 5:
+ if($this->debug)
+ $this->OutputDebug('Negotiating the authentication method ...');
+ $methods = 1;
+ $method = 0;
+ if(!fputs($this->connection, chr($version).chr($methods).chr($method)))
+ $error = $this->SetDataAccessError($send_error);
+ else
+ {
+ $response = fgets($this->connection, 3);
+ if(strlen($response) != 2)
+ $error = $this->SetDataAccessError($receive_error);
+ elseif(Ord($response[1]) != $method)
+ $error = 'the SOCKS server requires an authentication method that is not yet supported';
+ else
+ {
+ if($this->debug)
+ $this->OutputDebug('Connecting to '.$host_server_type.' server IP '.$host_ip.' port '.$host_port.'...');
+ $command = 1;
+ $address_type = 1;
+ if(!fputs($this->connection, chr($version).chr($command)."\x00".chr($address_type).pack('Nn', ip2long($host_ip), $host_port)))
+ $error = $this->SetDataAccessError($send_error);
+ else
+ {
+ $response = fgets($this->connection, 11);
+ if(strlen($response) != 10)
+ $error = $this->SetDataAccessError($receive_error);
+ else
+ {
+ $socks_errors = array(
+ "\x00"=>'',
+ "\x01"=>'general SOCKS server failure',
+ "\x02"=>'connection not allowed by ruleset',
+ "\x03"=>'Network unreachable',
+ "\x04"=>'Host unreachable',
+ "\x05"=>'Connection refused',
+ "\x06"=>'TTL expired',
+ "\x07"=>'Command not supported',
+ "\x08"=>'Address type not supported'
+ );
+ $error_code = $response[1];
+ $error = (IsSet($socks_errors[$error_code]) ? $socks_errors[$error_code] : 'unknown');
+ if(strlen($error))
+ $error = 'SOCKS error: '.$error;
+ }
+ }
+ }
+ }
+ break;
+ default:
+ $error = 'support for SOCKS protocol version '.$this->socks_version.' is not yet implemented';
+ break;
+ }
+ if(strlen($error))
+ {
+ fclose($this->connection);
+ return($error);
+ }
+ }
+ if($this->debug)
+ $this->OutputDebug("Connected to $host_name");
+ if(strlen($this->proxy_host_name)
+ && !strcmp(strtolower($this->protocol), 'https'))
+ {
+ if(function_exists('stream_socket_enable_crypto')
+ && in_array('ssl', stream_get_transports()))
+ $this->state = "ConnectedToProxy";
+ else
+ {
+ $this->OutputDebug("It is not possible to start SSL after connecting to the proxy server. If the proxy refuses to forward the SSL request, you may need to upgrade to PHP 5.1 or later with OpenSSL support enabled.");
+ $this->state="Connected";
+ }
+ }
+ else
+ $this->state="Connected";
+ return("");
+ }
+ }
+
+ Function Disconnect()
+ {
+ if($this->debug)
+ $this->OutputDebug("Disconnected from ".$this->connected_host);
+ if($this->use_curl)
+ {
+ curl_close($this->connection);
+ $this->response="";
+ }
+ else
+ fclose($this->connection);
+ $this->state="Disconnected";
+ return("");
+ }
+
+ /* Public methods */
+
+ Function GetRequestArguments($url, &$arguments)
+ {
+ $this->error = '';
+ $this->error_code = HTTP_CLIENT_ERROR_NO_ERROR;
+ $arguments=array();
+ $url = str_replace(' ', '%20', $url);
+ $parameters=@parse_url($url);
+ if(!$parameters)
+ return($this->SetError("it was not specified a valid URL", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if(!IsSet($parameters["scheme"]))
+ return($this->SetError("it was not specified the protocol type argument", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ switch(strtolower($parameters["scheme"]))
+ {
+ case "http":
+ case "https":
+ $arguments["Protocol"]=$parameters["scheme"];
+ break;
+ default:
+ return($parameters["scheme"]." connection scheme is not yet supported");
+ }
+ if(!IsSet($parameters["host"]))
+ return($this->SetError("it was not specified the connection host argument", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $arguments["HostName"]=$parameters["host"];
+ $arguments["Headers"]=array("Host"=>$parameters["host"].(IsSet($parameters["port"]) ? ":".$parameters["port"] : ""));
+ if(IsSet($parameters["user"]))
+ {
+ $arguments["AuthUser"]=UrlDecode($parameters["user"]);
+ if(!IsSet($parameters["pass"]))
+ $arguments["AuthPassword"]="";
+ }
+ if(IsSet($parameters["pass"]))
+ {
+ if(!IsSet($parameters["user"]))
+ $arguments["AuthUser"]="";
+ $arguments["AuthPassword"]=UrlDecode($parameters["pass"]);
+ }
+ if(IsSet($parameters["port"]))
+ {
+ if(strcmp($parameters["port"],strval(intval($parameters["port"]))))
+ return($this->SetError("it was not specified a valid connection host argument", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $arguments["HostPort"]=intval($parameters["port"]);
+ }
+ else
+ $arguments["HostPort"]=0;
+ $arguments["RequestURI"]=(IsSet($parameters["path"]) ? $parameters["path"] : "/").(IsSet($parameters["query"]) ? "?".$parameters["query"] : "");
+ if(strlen($this->user_agent))
+ $arguments["Headers"]["User-Agent"]=$this->user_agent;
+ if(strlen($this->accept))
+ $arguments["Headers"]["Accept"]=$this->accept;
+ return("");
+ }
+
+ Function Open($arguments)
+ {
+ if(strlen($this->error))
+ return($this->error);
+ $error_code = HTTP_CLIENT_ERROR_UNSPECIFIED_ERROR;
+ if(IsSet($arguments["HostName"]))
+ $this->host_name=$arguments["HostName"];
+ if(IsSet($arguments["HostPort"]))
+ $this->host_port=$arguments["HostPort"];
+ if(IsSet($arguments["ProxyHostName"]))
+ $this->proxy_host_name=$arguments["ProxyHostName"];
+ if(IsSet($arguments["ProxyHostPort"]))
+ $this->proxy_host_port=$arguments["ProxyHostPort"];
+ if(IsSet($arguments["SOCKSHostName"]))
+ $this->socks_host_name=$arguments["SOCKSHostName"];
+ if(IsSet($arguments["SOCKSHostPort"]))
+ $this->socks_host_port=$arguments["SOCKSHostPort"];
+ if(IsSet($arguments["SOCKSVersion"]))
+ $this->socks_version=$arguments["SOCKSVersion"];
+ if(IsSet($arguments["Protocol"]))
+ $this->protocol=$arguments["Protocol"];
+ switch(strtolower($this->protocol))
+ {
+ case "http":
+ $default_port=80;
+ break;
+ case "https":
+ $default_port=443;
+ break;
+ default:
+ return($this->SetError("it was not specified a valid connection protocol", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ if(strlen($this->proxy_host_name)==0)
+ {
+ if(strlen($this->host_name)==0)
+ return($this->SetError("it was not specified a valid hostname", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $host_name=$this->host_name;
+ $host_port=($this->host_port ? $this->host_port : $default_port);
+ $server_type = 'HTTP';
+ }
+ else
+ {
+ $host_name=$this->proxy_host_name;
+ $host_port=$this->proxy_host_port;
+ $server_type = 'HTTP proxy';
+ }
+ $ssl=(strtolower($this->protocol)=="https" && strlen($this->proxy_host_name)==0);
+ if($ssl
+ && strlen($this->socks_host_name))
+ return($this->SetError('establishing SSL connections via a SOCKS server is not yet supported', HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $this->use_curl=($ssl && $this->prefer_curl && function_exists("curl_init"));
+ switch($this->state)
+ {
+ case 'Connected':
+ if(!strcmp($host_name, $this->connected_host)
+ && intval($host_port) == $this->connected_port
+ && intval($ssl) == $this->connected_ssl)
+ {
+ if($this->debug)
+ $this->OutputDebug("Reusing connection to ".$this->connected_host);
+ return('');
+ }
+ if(strlen($error = $this->Disconnect()))
+ return($error);
+ case "Disconnected":
+ break;
+ default:
+ return("1 already connected");
+ }
+ if($this->debug)
+ $this->OutputDebug("Connecting to ".$this->host_name);
+ if($this->use_curl)
+ {
+ $error=(($this->connection=curl_init($this->protocol."://".$this->host_name.($host_port==$default_port ? "" : ":".strval($host_port))."/")) ? "" : "Could not initialize a CURL session");
+ if(strlen($error)==0)
+ {
+ if(IsSet($arguments["SSLCertificateFile"]))
+ curl_setopt($this->connection,CURLOPT_SSLCERT,$arguments["SSLCertificateFile"]);
+ if(IsSet($arguments["SSLCertificatePassword"]))
+ curl_setopt($this->connection,CURLOPT_SSLCERTPASSWD,$arguments["SSLCertificatePassword"]);
+ if(IsSet($arguments["SSLKeyFile"]))
+ curl_setopt($this->connection,CURLOPT_SSLKEY,$arguments["SSLKeyFile"]);
+ if(IsSet($arguments["SSLKeyPassword"]))
+ curl_setopt($this->connection,CURLOPT_SSLKEYPASSWD,$arguments["SSLKeyPassword"]);
+ }
+ $this->state="Connected";
+ }
+ else
+ {
+ $error="";
+ if(strlen($this->proxy_host_name)
+ && (IsSet($arguments["SSLCertificateFile"])
+ || IsSet($arguments["SSLCertificateFile"])))
+ $error="establishing SSL connections using certificates or private keys via non-SSL proxies is not supported";
+ else
+ {
+ if($ssl)
+ {
+ if(IsSet($arguments["SSLCertificateFile"]))
+ $error="establishing SSL connections using certificates is only supported when the cURL extension is enabled";
+ elseif(IsSet($arguments["SSLKeyFile"]))
+ $error="establishing SSL connections using a private key is only supported when the cURL extension is enabled";
+ else
+ {
+ $version=explode(".",function_exists("phpversion") ? phpversion() : "3.0.7");
+ $php_version=intval($version[0])*1000000+intval($version[1])*1000+intval($version[2]);
+ if($php_version<4003000)
+ $error="establishing SSL connections requires at least PHP version 4.3.0 or having the cURL extension enabled";
+ elseif(!function_exists("extension_loaded")
+ || !extension_loaded("openssl"))
+ $error="establishing SSL connections requires the OpenSSL extension enabled";
+ }
+ }
+ if(strlen($error)==0)
+ {
+ $error=$this->Connect($host_name, $host_port, $ssl, $server_type);
+ $error_code = $this->error_code;
+ }
+ }
+ }
+ if(strlen($error))
+ return($this->SetError($error, $error_code));
+ $this->session=md5(uniqid(""));
+ $this->connected_host = $host_name;
+ $this->connected_port = intval($host_port);
+ $this->connected_ssl = intval($ssl);
+ return("");
+ }
+
+ Function Close($force = 0)
+ {
+ if($this->state=="Disconnected")
+ return("1 already disconnected");
+ if(!$this->force_close
+ && $this->keep_alive
+ && !$force
+ && $this->state == 'ResponseReceived')
+ {
+ if($this->debug)
+ $this->OutputDebug('Keeping the connection alive to '.$this->connected_host);
+ $this->state = 'Connected';
+ return('');
+ }
+ return($this->Disconnect());
+ }
+
+ Function PickCookies(&$cookies,$secure)
+ {
+ if(IsSet($this->cookies[$secure]))
+ {
+ $now=gmdate("Y-m-d H-i-s");
+ for($domain=0,Reset($this->cookies[$secure]);$domain<count($this->cookies[$secure]);Next($this->cookies[$secure]),$domain++)
+ {
+ $domain_pattern=Key($this->cookies[$secure]);
+ $match=strlen($this->request_host)-strlen($domain_pattern);
+ if($match>=0
+ && !strcmp($domain_pattern,substr($this->request_host,$match))
+ && ($match==0
+ || $domain_pattern[0]=="."
+ || $this->request_host[$match-1]=="."))
+ {
+ for(Reset($this->cookies[$secure][$domain_pattern]),$path_part=0;$path_part<count($this->cookies[$secure][$domain_pattern]);Next($this->cookies[$secure][$domain_pattern]),$path_part++)
+ {
+ $path=Key($this->cookies[$secure][$domain_pattern]);
+ if(strlen($this->request_uri)>=strlen($path)
+ && substr($this->request_uri,0,strlen($path))==$path)
+ {
+ for(Reset($this->cookies[$secure][$domain_pattern][$path]),$cookie=0;$cookie<count($this->cookies[$secure][$domain_pattern][$path]);Next($this->cookies[$secure][$domain_pattern][$path]),$cookie++)
+ {
+ $cookie_name=Key($this->cookies[$secure][$domain_pattern][$path]);
+ $expires=$this->cookies[$secure][$domain_pattern][$path][$cookie_name]["expires"];
+ if($expires==""
+ || strcmp($now,$expires)<0)
+ $cookies[$cookie_name]=$this->cookies[$secure][$domain_pattern][$path][$cookie_name];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Function GetFileDefinition($file, &$definition)
+ {
+ $name="";
+ if(IsSet($file["FileName"]))
+ $name=basename($file["FileName"]);
+ if(IsSet($file["Name"]))
+ $name=$file["Name"];
+ if(strlen($name)==0)
+ return("it was not specified the file part name");
+ if(IsSet($file["Content-Type"]))
+ {
+ $content_type=$file["Content-Type"];
+ $type=$this->Tokenize(strtolower($content_type),"/");
+ $sub_type=$this->Tokenize("");
+ switch($type)
+ {
+ case "text":
+ case "image":
+ case "audio":
+ case "video":
+ case "application":
+ case "message":
+ break;
+ case "automatic":
+ switch($sub_type)
+ {
+ case "name":
+ switch(GetType($dot=strrpos($name,"."))=="integer" ? strtolower(substr($name,$dot)) : "")
+ {
+ case ".xls":
+ $content_type="application/excel";
+ break;
+ case ".hqx":
+ $content_type="application/macbinhex40";
+ break;
+ case ".doc":
+ case ".dot":
+ case ".wrd":
+ $content_type="application/msword";
+ break;
+ case ".pdf":
+ $content_type="application/pdf";
+ break;
+ case ".pgp":
+ $content_type="application/pgp";
+ break;
+ case ".ps":
+ case ".eps":
+ case ".ai":
+ $content_type="application/postscript";
+ break;
+ case ".ppt":
+ $content_type="application/powerpoint";
+ break;
+ case ".rtf":
+ $content_type="application/rtf";
+ break;
+ case ".tgz":
+ case ".gtar":
+ $content_type="application/x-gtar";
+ break;
+ case ".gz":
+ $content_type="application/x-gzip";
+ break;
+ case ".php":
+ case ".php3":
+ $content_type="application/x-httpd-php";
+ break;
+ case ".js":
+ $content_type="application/x-javascript";
+ break;
+ case ".ppd":
+ case ".psd":
+ $content_type="application/x-photoshop";
+ break;
+ case ".swf":
+ case ".swc":
+ case ".rf":
+ $content_type="application/x-shockwave-flash";
+ break;
+ case ".tar":
+ $content_type="application/x-tar";
+ break;
+ case ".zip":
+ $content_type="application/zip";
+ break;
+ case ".mid":
+ case ".midi":
+ case ".kar":
+ $content_type="audio/midi";
+ break;
+ case ".mp2":
+ case ".mp3":
+ case ".mpga":
+ $content_type="audio/mpeg";
+ break;
+ case ".ra":
+ $content_type="audio/x-realaudio";
+ break;
+ case ".wav":
+ $content_type="audio/wav";
+ break;
+ case ".bmp":
+ $content_type="image/bitmap";
+ break;
+ case ".gif":
+ $content_type="image/gif";
+ break;
+ case ".iff":
+ $content_type="image/iff";
+ break;
+ case ".jb2":
+ $content_type="image/jb2";
+ break;
+ case ".jpg":
+ case ".jpe":
+ case ".jpeg":
+ $content_type="image/jpeg";
+ break;
+ case ".jpx":
+ $content_type="image/jpx";
+ break;
+ case ".png":
+ $content_type="image/png";
+ break;
+ case ".tif":
+ case ".tiff":
+ $content_type="image/tiff";
+ break;
+ case ".wbmp":
+ $content_type="image/vnd.wap.wbmp";
+ break;
+ case ".xbm":
+ $content_type="image/xbm";
+ break;
+ case ".css":
+ $content_type="text/css";
+ break;
+ case ".txt":
+ $content_type="text/plain";
+ break;
+ case ".htm":
+ case ".html":
+ $content_type="text/html";
+ break;
+ case ".xml":
+ $content_type="text/xml";
+ break;
+ case ".mpg":
+ case ".mpe":
+ case ".mpeg":
+ $content_type="video/mpeg";
+ break;
+ case ".qt":
+ case ".mov":
+ $content_type="video/quicktime";
+ break;
+ case ".avi":
+ $content_type="video/x-ms-video";
+ break;
+ case ".eml":
+ $content_type="message/rfc822";
+ break;
+ default:
+ $content_type="application/octet-stream";
+ break;
+ }
+ break;
+ default:
+ return($content_type." is not a supported automatic content type detection method");
+ }
+ break;
+ default:
+ return($content_type." is not a supported file content type");
+ }
+ }
+ else
+ $content_type="application/octet-stream";
+ $definition=array(
+ "Content-Type"=>$content_type,
+ "NAME"=>$name
+ );
+ if(IsSet($file["FileName"]))
+ {
+ if(GetType($length=@filesize($file["FileName"]))!="integer")
+ {
+ $error="it was not possible to determine the length of the file ".$file["FileName"];
+ if(IsSet($php_errormsg)
+ && strlen($php_errormsg))
+ $error.=": ".$php_errormsg;
+ if(!file_exists($file["FileName"]))
+ $error="it was not possible to access the file ".$file["FileName"];
+ return($error);
+ }
+ $definition["FILENAME"]=$file["FileName"];
+ $definition["Content-Length"]=$length;
+ }
+ elseif(IsSet($file["Data"]))
+ $definition["Content-Length"]=strlen($definition["DATA"]=$file["Data"]);
+ else
+ return("it was not specified a valid file name");
+ return("");
+ }
+
+ Function ConnectFromProxy($arguments, &$headers)
+ {
+ if(!$this->PutLine('CONNECT '.$this->host_name.':'.($this->host_port ? $this->host_port : 443).' HTTP/1.0')
+ || (strlen($this->user_agent)
+ && !$this->PutLine('User-Agent: '.$this->user_agent))
+ || (strlen($this->accept)
+ && !$this->PutLine('Accept: '.$this->accept))
+ || (IsSet($arguments['Headers']['Proxy-Authorization'])
+ && !$this->PutLine('Proxy-Authorization: '.$arguments['Headers']['Proxy-Authorization']))
+ || !$this->PutLine(''))
+ {
+ $this->Disconnect();
+ return($this->error);
+ }
+ $this->state = "ConnectSent";
+ if(strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($error);
+ $proxy_authorization="";
+ while(!strcmp($this->response_status, "100"))
+ {
+ $this->state="ConnectSent";
+ if(strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($error);
+ }
+ switch($this->response_status)
+ {
+ case "200":
+ if(!@stream_socket_enable_crypto($this->connection, 1, STREAM_CRYPTO_METHOD_SSLv23_CLIENT))
+ {
+ $this->SetPHPError('it was not possible to start a SSL encrypted connection via this proxy', $php_errormsg, HTTP_CLIENT_ERROR_COMMUNICATION_FAILURE);
+ $this->Disconnect();
+ return($this->error);
+ }
+ $this->state = "Connected";
+ break;
+ case "407":
+ if(strlen($error=$this->Authenticate($headers, -1, $proxy_authorization, $this->proxy_request_user, $this->proxy_request_password, $this->proxy_request_realm, $this->proxy_request_workstation)))
+ return($error);
+ break;
+ default:
+ return($this->SetError("unable to send request via proxy", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ }
+ return("");
+ }
+
+ Function SendRequest($arguments)
+ {
+ if(strlen($this->error))
+ return($this->error);
+ if(IsSet($arguments["ProxyUser"]))
+ $this->proxy_request_user=$arguments["ProxyUser"];
+ elseif(IsSet($this->proxy_user))
+ $this->proxy_request_user=$this->proxy_user;
+ if(IsSet($arguments["ProxyPassword"]))
+ $this->proxy_request_password=$arguments["ProxyPassword"];
+ elseif(IsSet($this->proxy_password))
+ $this->proxy_request_password=$this->proxy_password;
+ if(IsSet($arguments["ProxyRealm"]))
+ $this->proxy_request_realm=$arguments["ProxyRealm"];
+ elseif(IsSet($this->proxy_realm))
+ $this->proxy_request_realm=$this->proxy_realm;
+ if(IsSet($arguments["ProxyWorkstation"]))
+ $this->proxy_request_workstation=$arguments["ProxyWorkstation"];
+ elseif(IsSet($this->proxy_workstation))
+ $this->proxy_request_workstation=$this->proxy_workstation;
+ switch($this->state)
+ {
+ case "Disconnected":
+ return($this->SetError("connection was not yet established", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "Connected":
+ $connect = 0;
+ break;
+ case "ConnectedToProxy":
+ if(strlen($error = $this->ConnectFromProxy($arguments, $headers)))
+ return($error);
+ $connect = 1;
+ break;
+ default:
+ return($this->SetError("can not send request in the current connection state", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ if(IsSet($arguments["RequestMethod"]))
+ $this->request_method=$arguments["RequestMethod"];
+ if(IsSet($arguments["User-Agent"]))
+ $this->user_agent=$arguments["User-Agent"];
+ if(!IsSet($arguments["Headers"]["User-Agent"])
+ && strlen($this->user_agent))
+ $arguments["Headers"]["User-Agent"]=$this->user_agent;
+ if(IsSet($arguments["KeepAlive"]))
+ $this->keep_alive=intval($arguments["KeepAlive"]);
+ if(!IsSet($arguments["Headers"]["Connection"])
+ && $this->keep_alive)
+ $arguments["Headers"]["Connection"]='Keep-Alive';
+ if(IsSet($arguments["Accept"]))
+ $this->user_agent=$arguments["Accept"];
+ if(!IsSet($arguments["Headers"]["Accept"])
+ && strlen($this->accept))
+ $arguments["Headers"]["Accept"]=$this->accept;
+ if(strlen($this->request_method)==0)
+ return($this->SetError("it was not specified a valid request method", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if(IsSet($arguments["RequestURI"]))
+ $this->request_uri=$arguments["RequestURI"];
+ if(strlen($this->request_uri)==0
+ || substr($this->request_uri,0,1)!="/")
+ return($this->SetError("it was not specified a valid request URI", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $this->request_arguments=$arguments;
+ $this->request_headers=(IsSet($arguments["Headers"]) ? $arguments["Headers"] : array());
+ $body_length=0;
+ $this->request_body="";
+ $get_body=1;
+ if($this->request_method=="POST"
+ || $this->request_method=="PUT")
+ {
+ if(IsSet($arguments['StreamRequest']))
+ {
+ $get_body = 0;
+ $this->request_headers["Transfer-Encoding"]="chunked";
+ }
+ elseif(IsSet($arguments["PostFiles"])
+ || ($this->force_multipart_form_post
+ && IsSet($arguments["PostValues"])))
+ {
+ $boundary="--".md5(uniqid(time()));
+ $this->request_headers["Content-Type"]="multipart/form-data; boundary=".$boundary.(IsSet($arguments["CharSet"]) ? "; charset=".$arguments["CharSet"] : "");
+ $post_parts=array();
+ if(IsSet($arguments["PostValues"]))
+ {
+ $values=$arguments["PostValues"];
+ if(GetType($values)!="array")
+ return($this->SetError("it was not specified a valid POST method values array", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ for(Reset($values),$value=0;$value<count($values);Next($values),$value++)
+ {
+ $input=Key($values);
+ $headers="--".$boundary."\r\nContent-Disposition: form-data; name=\"".$input."\"\r\n\r\n";
+ $data=$values[$input];
+ $post_parts[]=array("HEADERS"=>$headers,"DATA"=>$data);
+ $body_length+=strlen($headers)+strlen($data)+strlen("\r\n");
+ }
+ }
+ $body_length+=strlen("--".$boundary."--\r\n");
+ $files=(IsSet($arguments["PostFiles"]) ? $arguments["PostFiles"] : array());
+ Reset($files);
+ $end=(GetType($input=Key($files))!="string");
+ for(;!$end;)
+ {
+ if(strlen($error=$this->GetFileDefinition($files[$input],$definition)))
+ return("3 ".$error);
+ $headers="--".$boundary."\r\nContent-Disposition: form-data; name=\"".$input."\"; filename=\"".$definition["NAME"]."\"\r\nContent-Type: ".$definition["Content-Type"]."\r\n\r\n";
+ $part=count($post_parts);
+ $post_parts[$part]=array("HEADERS"=>$headers);
+ if(IsSet($definition["FILENAME"]))
+ {
+ $post_parts[$part]["FILENAME"]=$definition["FILENAME"];
+ $data="";
+ }
+ else
+ $data=$definition["DATA"];
+ $post_parts[$part]["DATA"]=$data;
+ $body_length+=strlen($headers)+$definition["Content-Length"]+strlen("\r\n");
+ Next($files);
+ $end=(GetType($input=Key($files))!="string");
+ }
+ $get_body=0;
+ }
+ elseif(IsSet($arguments["PostValues"]))
+ {
+ $values=$arguments["PostValues"];
+ if(GetType($values)!="array")
+ return($this->SetError("it was not specified a valid POST method values array", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ for(Reset($values),$value=0;$value<count($values);Next($values),$value++)
+ {
+ $k=Key($values);
+ if(GetType($values[$k])=="array")
+ {
+ for($v = 0; $v < count($values[$k]); $v++)
+ {
+ if($value+$v>0)
+ $this->request_body.="&";
+ $this->request_body.=UrlEncode($k)."=".UrlEncode($values[$k][$v]);
+ }
+ }
+ else
+ {
+ if($value>0)
+ $this->request_body.="&";
+ $this->request_body.=UrlEncode($k)."=".UrlEncode($values[$k]);
+ }
+ }
+ $this->request_headers["Content-Type"]="application/x-www-form-urlencoded".(IsSet($arguments["CharSet"]) ? "; charset=".$arguments["CharSet"] : "");
+ $get_body=0;
+ }
+ }
+ if($get_body
+ && (IsSet($arguments["Body"])
+ || IsSet($arguments["BodyStream"])))
+ {
+ if(IsSet($arguments["Body"]))
+ $this->request_body=$arguments["Body"];
+ else
+ {
+ $stream=$arguments["BodyStream"];
+ $this->request_body="";
+ for($part=0; $part<count($stream); $part++)
+ {
+ if(IsSet($stream[$part]["Data"]))
+ $this->request_body.=$stream[$part]["Data"];
+ elseif(IsSet($stream[$part]["File"]))
+ {
+ if(!($file=@fopen($stream[$part]["File"],"rb")))
+ return($this->SetPHPError("could not open upload file ".$stream[$part]["File"], $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE));
+ while(!feof($file))
+ {
+ if(GetType($block=@fread($file,$this->file_buffer_length))!="string")
+ {
+ $error=$this->SetPHPError("could not read body stream file ".$stream[$part]["File"], $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE);
+ fclose($file);
+ return($error);
+ }
+ $this->request_body.=$block;
+ }
+ fclose($file);
+ }
+ else
+ return("5 it was not specified a valid file or data body stream element at position ".$part);
+ }
+ }
+ if(!IsSet($this->request_headers["Content-Type"]))
+ $this->request_headers["Content-Type"]="application/octet-stream".(IsSet($arguments["CharSet"]) ? "; charset=".$arguments["CharSet"] : "");
+ }
+ if(IsSet($arguments["AuthUser"]))
+ $this->request_user=$arguments["AuthUser"];
+ elseif(IsSet($this->user))
+ $this->request_user=$this->user;
+ if(IsSet($arguments["AuthPassword"]))
+ $this->request_password=$arguments["AuthPassword"];
+ elseif(IsSet($this->password))
+ $this->request_password=$this->password;
+ if(IsSet($arguments["AuthRealm"]))
+ $this->request_realm=$arguments["AuthRealm"];
+ elseif(IsSet($this->realm))
+ $this->request_realm=$this->realm;
+ if(IsSet($arguments["AuthWorkstation"]))
+ $this->request_workstation=$arguments["AuthWorkstation"];
+ elseif(IsSet($this->workstation))
+ $this->request_workstation=$this->workstation;
+ if(strlen($this->proxy_host_name)==0
+ || $connect)
+ $request_uri=$this->request_uri;
+ else
+ {
+ switch(strtolower($this->protocol))
+ {
+ case "http":
+ $default_port=80;
+ break;
+ case "https":
+ $default_port=443;
+ break;
+ }
+ $request_uri=strtolower($this->protocol)."://".$this->host_name.(($this->host_port==0 || $this->host_port==$default_port) ? "" : ":".$this->host_port).$this->request_uri;
+ }
+ if($this->use_curl)
+ {
+ $version=(GetType($v=curl_version())=="array" ? (IsSet($v["version"]) ? $v["version"] : "0.0.0") : (preg_match("/^libcurl\\/([0-9]+\\.[0-9]+\\.[0-9]+)/",$v,$m) ? $m[1] : "0.0.0"));
+ $curl_version=100000*intval($this->Tokenize($version,"."))+1000*intval($this->Tokenize("."))+intval($this->Tokenize(""));
+ $protocol_version=($curl_version<713002 ? "1.0" : $this->protocol_version);
+ }
+ else
+ $protocol_version=$this->protocol_version;
+ $this->request=$this->request_method." ".$request_uri." HTTP/".$protocol_version;
+ if($body_length
+ || ($body_length=strlen($this->request_body))
+ || !strcmp($this->request_method, 'POST'))
+ $this->request_headers["Content-Length"]=$body_length;
+ for($headers=array(),$host_set=0,Reset($this->request_headers),$header=0;$header<count($this->request_headers);Next($this->request_headers),$header++)
+ {
+ $header_name=Key($this->request_headers);
+ $header_value=$this->request_headers[$header_name];
+ if(GetType($header_value)=="array")
+ {
+ for(Reset($header_value),$value=0;$value<count($header_value);Next($header_value),$value++)
+ $headers[]=$header_name.": ".$header_value[Key($header_value)];
+ }
+ else
+ $headers[]=$header_name.": ".$header_value;
+ if(strtolower(Key($this->request_headers))=="host")
+ {
+ $this->request_host=strtolower($header_value);
+ $host_set=1;
+ }
+ }
+ if(!$host_set)
+ {
+ $headers[]="Host: ".$this->host_name;
+ $this->request_host=strtolower($this->host_name);
+ }
+ if(count($this->cookies))
+ {
+ $cookies=array();
+ $this->PickCookies($cookies,0);
+ if(strtolower($this->protocol)=="https")
+ $this->PickCookies($cookies,1);
+ if(count($cookies))
+ {
+ $h=count($headers);
+ $headers[$h]="Cookie:";
+ for(Reset($cookies),$cookie=0;$cookie<count($cookies);Next($cookies),$cookie++)
+ {
+ $cookie_name=Key($cookies);
+ $headers[$h].=" ".$cookie_name."=".$cookies[$cookie_name]["value"].";";
+ }
+ }
+ }
+ $next_state = "RequestSent";
+ if($this->use_curl)
+ {
+ if(IsSet($arguments['StreamRequest']))
+ return($this->SetError("Streaming request data is not supported when using Curl", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if($body_length
+ && strlen($this->request_body)==0)
+ {
+ for($request_body="",$success=1,$part=0;$part<count($post_parts);$part++)
+ {
+ $request_body.=$post_parts[$part]["HEADERS"].$post_parts[$part]["DATA"];
+ if(IsSet($post_parts[$part]["FILENAME"]))
+ {
+ if(!($file=@fopen($post_parts[$part]["FILENAME"],"rb")))
+ {
+ $this->SetPHPError("could not open upload file ".$post_parts[$part]["FILENAME"], $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE);
+ $success=0;
+ break;
+ }
+ while(!feof($file))
+ {
+ if(GetType($block=@fread($file,$this->file_buffer_length))!="string")
+ {
+ $this->SetPHPError("could not read upload file", $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE);
+ $success=0;
+ break;
+ }
+ $request_body.=$block;
+ }
+ fclose($file);
+ if(!$success)
+ break;
+ }
+ $request_body.="\r\n";
+ }
+ $request_body.="--".$boundary."--\r\n";
+ }
+ else
+ $request_body=$this->request_body;
+ curl_setopt($this->connection,CURLOPT_HEADER,1);
+ curl_setopt($this->connection,CURLOPT_RETURNTRANSFER,1);
+ if($this->timeout)
+ curl_setopt($this->connection,CURLOPT_TIMEOUT,$this->timeout);
+ curl_setopt($this->connection,CURLOPT_SSL_VERIFYPEER,0);
+ curl_setopt($this->connection,CURLOPT_SSL_VERIFYHOST,0);
+ $request=$this->request."\r\n".implode("\r\n",$headers)."\r\n\r\n".$request_body;
+ curl_setopt($this->connection,CURLOPT_CUSTOMREQUEST,$request);
+ if($this->debug)
+ $this->OutputDebug("C ".$request);
+ if(!($success=(strlen($this->response=curl_exec($this->connection))!=0)))
+ {
+ $error=curl_error($this->connection);
+ $this->SetError("Could not execute the request".(strlen($error) ? ": ".$error : ""), HTTP_CLIENT_ERROR_PROTOCOL_FAILURE);
+ }
+ }
+ else
+ {
+ if(($success=$this->PutLine($this->request)))
+ {
+ for($header=0;$header<count($headers);$header++)
+ {
+ if(!$success=$this->PutLine($headers[$header]))
+ break;
+ }
+ if($success
+ && ($success=$this->PutLine("")))
+ {
+ if(IsSet($arguments['StreamRequest']))
+ $next_state = "SendingRequestBody";
+ elseif($body_length)
+ {
+ if(strlen($this->request_body))
+ $success=$this->PutData($this->request_body);
+ else
+ {
+ for($part=0;$part<count($post_parts);$part++)
+ {
+ if(!($success=$this->PutData($post_parts[$part]["HEADERS"]))
+ || !($success=$this->PutData($post_parts[$part]["DATA"])))
+ break;
+ if(IsSet($post_parts[$part]["FILENAME"]))
+ {
+ if(!($file=@fopen($post_parts[$part]["FILENAME"],"rb")))
+ {
+ $this->SetPHPError("could not open upload file ".$post_parts[$part]["FILENAME"], $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE);
+ $success=0;
+ break;
+ }
+ while(!feof($file))
+ {
+ if(GetType($block=@fread($file,$this->file_buffer_length))!="string")
+ {
+ $this->SetPHPError("could not read upload file", $php_errormsg, HTTP_CLIENT_ERROR_CANNOT_ACCESS_LOCAL_FILE);
+ $success=0;
+ break;
+ }
+ if(!($success=$this->PutData($block)))
+ break;
+ }
+ fclose($file);
+ if(!$success)
+ break;
+ }
+ if(!($success=$this->PutLine("")))
+ break;
+ }
+ if($success)
+ $success=$this->PutLine("--".$boundary."--");
+ }
+ if($success)
+ $sucess=$this->FlushData();
+ }
+ }
+ }
+ }
+ if(!$success)
+ return($this->SetError("could not send the HTTP request: ".$this->error, $this->error_code));
+ $this->state=$next_state;
+ return("");
+ }
+
+ Function SetCookie($name, $value, $expires="" , $path="/" , $domain="" , $secure=0, $verbatim=0)
+ {
+ if(strlen($this->error))
+ return($this->error);
+ if(strlen($name)==0)
+ return($this->SetError("it was not specified a valid cookie name", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if(strlen($path)==0
+ || strcmp($path[0],"/"))
+ return($this->SetError($path." is not a valid path for setting cookie ".$name, HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if($domain==""
+ || !strpos($domain,".",$domain[0]=="." ? 1 : 0))
+ return($this->SetError($domain." is not a valid domain for setting cookie ".$name, HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $domain=strtolower($domain);
+ if(!strcmp($domain[0],"."))
+ $domain=substr($domain,1);
+ if(!$verbatim)
+ {
+ $name=$this->CookieEncode($name,1);
+ $value=$this->CookieEncode($value,0);
+ }
+ $secure=intval($secure);
+ $this->cookies[$secure][$domain][$path][$name]=array(
+ "name"=>$name,
+ "value"=>$value,
+ "domain"=>$domain,
+ "path"=>$path,
+ "expires"=>$expires,
+ "secure"=>$secure
+ );
+ return("");
+ }
+
+ Function SendRequestBody($data, $end_of_data)
+ {
+ if(strlen($this->error))
+ return($this->error);
+ switch($this->state)
+ {
+ case "Disconnected":
+ return($this->SetError("connection was not yet established", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "Connected":
+ case "ConnectedToProxy":
+ return($this->SetError("request was not sent", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "SendingRequestBody":
+ break;
+ case "RequestSent":
+ return($this->SetError("request body was already sent", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ default:
+ return($this->SetError("can not send the request body in the current connection state", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ $length = strlen($data);
+ if($length)
+ {
+ $size = dechex($length)."\r\n";
+ if(!$this->PutData($size)
+ || !$this->PutData($data))
+ return($this->error);
+ }
+ if($end_of_data)
+ {
+ $size = "0\r\n";
+ if(!$this->PutData($size))
+ return($this->error);
+ $this->state = "RequestSent";
+ }
+ return("");
+ }
+
+ Function ReadReplyHeadersResponse(&$headers)
+ {
+ $headers=array();
+ if(strlen($this->error))
+ return($this->error);
+ switch($this->state)
+ {
+ case "Disconnected":
+ return($this->SetError("connection was not yet established", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "Connected":
+ return($this->SetError("request was not sent", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "ConnectedToProxy":
+ return($this->SetError("connection from the remote server from the proxy was not yet established", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "SendingRequestBody":
+ return($this->SetError("request body data was not completely sent", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "ConnectSent":
+ $connect = 1;
+ break;
+ case "RequestSent":
+ $connect = 0;
+ break;
+ default:
+ return($this->SetError("can not get request headers in the current connection state", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ $this->content_length=$this->read_length=$this->read_response=$this->remaining_chunk=0;
+ $this->content_length_set=$this->chunked=$this->last_chunk_read=$chunked=0;
+ $this->force_close = $this->connection_close=0;
+ for($this->response_status="";;)
+ {
+ $line=$this->GetLine();
+ if(GetType($line)!="string")
+ return($this->SetError("could not read request reply: ".$this->error, $this->error_code));
+ if(strlen($this->response_status)==0)
+ {
+ if(!preg_match($match="/^http\\/[0-9]+\\.[0-9]+[ \t]+([0-9]+)[ \t]*(.*)\$/i",$line,$matches))
+ return($this->SetError("it was received an unexpected HTTP response status", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ $this->response_status=$matches[1];
+ $this->response_message=$matches[2];
+ }
+ if($line=="")
+ {
+ if(strlen($this->response_status)==0)
+ return($this->SetError("it was not received HTTP response status", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ $this->state=($connect ? "GotConnectHeaders" : "GotReplyHeaders");
+ break;
+ }
+ $header_name=strtolower($this->Tokenize($line,":"));
+ $header_value=Trim(Chop($this->Tokenize("\r\n")));
+ if(IsSet($headers[$header_name]))
+ {
+ if(GetType($headers[$header_name])=="string")
+ $headers[$header_name]=array($headers[$header_name]);
+ $headers[$header_name][]=$header_value;
+ }
+ else
+ $headers[$header_name]=$header_value;
+ if(!$connect)
+ {
+ switch($header_name)
+ {
+ case "content-length":
+ $this->content_length=intval($headers[$header_name]);
+ $this->content_length_set=1;
+ break;
+ case "transfer-encoding":
+ $encoding=$this->Tokenize($header_value,"; \t");
+ if(!$this->use_curl
+ && !strcmp($encoding,"chunked"))
+ $chunked=1;
+ break;
+ case "set-cookie":
+ if($this->support_cookies)
+ {
+ if(GetType($headers[$header_name])=="array")
+ $cookie_headers=$headers[$header_name];
+ else
+ $cookie_headers=array($headers[$header_name]);
+ for($cookie=0;$cookie<count($cookie_headers);$cookie++)
+ {
+ $cookie_name=trim($this->Tokenize($cookie_headers[$cookie],"="));
+ $cookie_value=$this->Tokenize(";");
+ $domain=$this->request_host;
+ $path="/";
+ $expires="";
+ $secure=0;
+ while(($name = strtolower(trim(UrlDecode($this->Tokenize("=")))))!="")
+ {
+ $value=UrlDecode($this->Tokenize(";"));
+ switch($name)
+ {
+ case "domain":
+ $domain=$value;
+ break;
+ case "path":
+ $path=$value;
+ break;
+ case "expires":
+ if(preg_match("/^((Mon|Monday|Tue|Tuesday|Wed|Wednesday|Thu|Thursday|Fri|Friday|Sat|Saturday|Sun|Sunday), )?([0-9]{2})\\-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\-([0-9]{2,4}) ([0-9]{2})\\:([0-9]{2})\\:([0-9]{2}) GMT\$/",$value,$matches))
+ {
+ $year=intval($matches[5]);
+ if($year<1900)
+ $year+=($year<70 ? 2000 : 1900);
+ $expires="$year-".$this->months[$matches[4]]."-".$matches[3]." ".$matches[6].":".$matches[7].":".$matches[8];
+ }
+ break;
+ case "secure":
+ $secure=1;
+ break;
+ }
+ }
+ if(strlen($this->SetCookie($cookie_name, $cookie_value, $expires, $path , $domain, $secure, 1)))
+ $this->error="";
+ }
+ }
+ break;
+ case "connection":
+ $this->force_close = $this->connection_close=!strcmp(strtolower($header_value),"close");
+ break;
+ }
+ }
+ }
+ $this->chunked=$chunked;
+ if($this->content_length_set)
+ $this->connection_close=0;
+ return("");
+ }
+
+ Function Redirect(&$headers)
+ {
+ if($this->follow_redirect)
+ {
+ if(!IsSet($headers["location"])
+ || (GetType($headers["location"])!="array"
+ && strlen($location=$headers["location"])==0)
+ || (GetType($headers["location"])=="array"
+ && strlen($location=$headers["location"][0])==0))
+ return($this->SetError("it was received a redirect without location URL", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ if(strcmp($location[0],"/"))
+ {
+ if(!($location_arguments=@parse_url($location)))
+ return($this->SetError("the server did not return a valid redirection location URL", HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ if(!IsSet($location_arguments["scheme"]))
+ $location=((GetType($end=strrpos($this->request_uri,"/"))=="integer" && $end>1) ? substr($this->request_uri,0,$end) : "")."/".$location;
+ }
+ if(!strcmp($location[0],"/"))
+ $location=$this->protocol."://".$this->host_name.($this->host_port ? ":".$this->host_port : "").$location;
+ $error=$this->GetRequestArguments($location,$arguments);
+ if(strlen($error))
+ return($this->SetError("could not process redirect url: ".$error, HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ $arguments["RequestMethod"]="GET";
+ if(strlen($error=$this->Close())==0
+ && strlen($error=$this->Open($arguments))==0
+ && strlen($error=$this->SendRequest($arguments))==0)
+ {
+ $this->redirection_level++;
+ if($this->redirection_level>$this->redirection_limit)
+ {
+ $error="it was exceeded the limit of request redirections";
+ $this->error_code = HTTP_CLIENT_ERROR_PROTOCOL_FAILURE;
+ }
+ else
+ $error=$this->ReadReplyHeaders($headers);
+ $this->redirection_level--;
+ }
+ if(strlen($error))
+ return($this->SetError($error, $this->error_code));
+ }
+ return("");
+ }
+
+ Function Authenticate(&$headers, $proxy, &$proxy_authorization, &$user, &$password, &$realm, &$workstation)
+ {
+ if($proxy)
+ {
+ $authenticate_header="proxy-authenticate";
+ $authorization_header="Proxy-Authorization";
+ $authenticate_status="407";
+ $authentication_mechanism=$this->proxy_authentication_mechanism;
+ }
+ else
+ {
+ $authenticate_header="www-authenticate";
+ $authorization_header="Authorization";
+ $authenticate_status="401";
+ $authentication_mechanism=$this->authentication_mechanism;
+ }
+ if(IsSet($headers[$authenticate_header])
+ && $this->sasl_authenticate)
+ {
+ if(function_exists("class_exists")
+ && !class_exists("sasl_client_class"))
+ return($this->SetError("the SASL client class needs to be loaded to be able to authenticate".($proxy ? " with the proxy server" : "")." and access this site", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ if(GetType($headers[$authenticate_header])=="array")
+ $authenticate=$headers[$authenticate_header];
+ else
+ $authenticate=array($headers[$authenticate_header]);
+ for($response="", $mechanisms=array(),$m=0;$m<count($authenticate);$m++)
+ {
+ $mechanism=$this->Tokenize($authenticate[$m]," ");
+ $response=$this->Tokenize("");
+ if(strlen($authentication_mechanism))
+ {
+ if(!strcmp($authentication_mechanism,$mechanism))
+ {
+ $mechanisms[]=$mechanism;
+ break;
+ }
+ }
+ else
+ $mechanisms[]=$mechanism;
+ }
+ $sasl=new sasl_client_class;
+ if(IsSet($user))
+ $sasl->SetCredential("user",$user);
+ if(IsSet($password))
+ $sasl->SetCredential("password",$password);
+ if(IsSet($realm))
+ $sasl->SetCredential("realm",$realm);
+ if(IsSet($workstation))
+ $sasl->SetCredential("workstation",$workstation);
+ $sasl->SetCredential("uri",$this->request_uri);
+ $sasl->SetCredential("method",$this->request_method);
+ $sasl->SetCredential("session",$this->session);
+ do
+ {
+ $status=$sasl->Start($mechanisms,$message,$interactions);
+ }
+ while($status==SASL_INTERACT);
+ switch($status)
+ {
+ case SASL_CONTINUE:
+ break;
+ case SASL_NOMECH:
+ return($this->SetError(($proxy ? "proxy " : "")."authentication error: ".(strlen($authentication_mechanism) ? "authentication mechanism ".$authentication_mechanism." may not be used: " : "").$sasl->error, HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ default:
+ return($this->SetError("Could not start the SASL ".($proxy ? "proxy " : "")."authentication client: ".$sasl->error, HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ if($proxy >= 0)
+ {
+ for(;;)
+ {
+ if(strlen($error=$this->ReadReplyBody($body,$this->file_buffer_length)))
+ return($error);
+ if(strlen($body)==0)
+ break;
+ }
+ }
+ $authorization_value=$sasl->mechanism.(IsSet($message) ? " ".($sasl->encode_response ? base64_encode($message) : $message) : "");
+ $request_arguments=$this->request_arguments;
+ $arguments=$request_arguments;
+ $arguments["Headers"][$authorization_header]=$authorization_value;
+ if(!$proxy
+ && strlen($proxy_authorization))
+ $arguments["Headers"]["Proxy-Authorization"]=$proxy_authorization;
+ if(strlen($error=$this->Close())
+ || strlen($error=$this->Open($arguments)))
+ return($this->SetError($error, $this->error_code));
+ $authenticated=0;
+ if(IsSet($message))
+ {
+ if($proxy < 0)
+ {
+ if(strlen($error=$this->ConnectFromProxy($arguments, $headers)))
+ return($this->SetError($error, $this->error_code));
+ }
+ else
+ {
+ if(strlen($error=$this->SendRequest($arguments))
+ || strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($this->SetError($error, $this->error_code));
+ }
+ if(!IsSet($headers[$authenticate_header]))
+ $authenticate=array();
+ elseif(GetType($headers[$authenticate_header])=="array")
+ $authenticate=$headers[$authenticate_header];
+ else
+ $authenticate=array($headers[$authenticate_header]);
+ for($mechanism=0;$mechanism<count($authenticate);$mechanism++)
+ {
+ if(!strcmp($this->Tokenize($authenticate[$mechanism]," "),$sasl->mechanism))
+ {
+ $response=$this->Tokenize("");
+ break;
+ }
+ }
+ switch($this->response_status)
+ {
+ case $authenticate_status:
+ break;
+ case "301":
+ case "302":
+ case "303":
+ case "307":
+ if($proxy >= 0)
+ return($this->Redirect($headers));
+ default:
+ if(intval($this->response_status/100)==2)
+ {
+ if($proxy)
+ $proxy_authorization=$authorization_value;
+ $authenticated=1;
+ break;
+ }
+ if($proxy
+ && !strcmp($this->response_status,"401"))
+ {
+ $proxy_authorization=$authorization_value;
+ $authenticated=1;
+ break;
+ }
+ return($this->SetError(($proxy ? "proxy " : "")."authentication error: ".$this->response_status." ".$this->response_message, HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ }
+ }
+ for(;!$authenticated;)
+ {
+ do
+ {
+ $status=$sasl->Step($response,$message,$interactions);
+ }
+ while($status==SASL_INTERACT);
+ switch($status)
+ {
+ case SASL_CONTINUE:
+ $authorization_value=$sasl->mechanism.(IsSet($message) ? " ".($sasl->encode_response ? base64_encode($message) : $message) : "");
+ $arguments=$request_arguments;
+ $arguments["Headers"][$authorization_header]=$authorization_value;
+ if(!$proxy
+ && strlen($proxy_authorization))
+ $arguments["Headers"]["Proxy-Authorization"]=$proxy_authorization;
+ if($proxy < 0)
+ {
+ if(strlen($error=$this->ConnectFromProxy($arguments, $headers)))
+ return($this->SetError($error, $this->error_code));
+ }
+ else
+ {
+ if(strlen($error=$this->SendRequest($arguments))
+ || strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($this->SetError($error, $this->error_code));
+ }
+ switch($this->response_status)
+ {
+ case $authenticate_status:
+ if(GetType($headers[$authenticate_header])=="array")
+ $authenticate=$headers[$authenticate_header];
+ else
+ $authenticate=array($headers[$authenticate_header]);
+ for($response="",$mechanism=0;$mechanism<count($authenticate);$mechanism++)
+ {
+ if(!strcmp($this->Tokenize($authenticate[$mechanism]," "),$sasl->mechanism))
+ {
+ $response=$this->Tokenize("");
+ break;
+ }
+ }
+ if($proxy >= 0)
+ {
+ for(;;)
+ {
+ if(strlen($error=$this->ReadReplyBody($body,$this->file_buffer_length)))
+ return($error);
+ if(strlen($body)==0)
+ break;
+ }
+ }
+ $this->state="Connected";
+ break;
+ case "301":
+ case "302":
+ case "303":
+ case "307":
+ if($proxy >= 0)
+ return($this->Redirect($headers));
+ default:
+ if(intval($this->response_status/100)==2)
+ {
+ if($proxy)
+ $proxy_authorization=$authorization_value;
+ $authenticated=1;
+ break;
+ }
+ if($proxy
+ && !strcmp($this->response_status,"401"))
+ {
+ $proxy_authorization=$authorization_value;
+ $authenticated=1;
+ break;
+ }
+ return($this->SetError(($proxy ? "proxy " : "")."authentication error: ".$this->response_status." ".$this->response_message));
+ }
+ break;
+ default:
+ return($this->SetError("Could not process the SASL ".($proxy ? "proxy " : "")."authentication step: ".$sasl->error, HTTP_CLIENT_ERROR_PROTOCOL_FAILURE));
+ }
+ }
+ }
+ return("");
+ }
+
+ Function ReadReplyHeaders(&$headers)
+ {
+ if(strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($error);
+ $proxy_authorization="";
+ while(!strcmp($this->response_status, "100"))
+ {
+ $this->state="RequestSent";
+ if(strlen($error=$this->ReadReplyHeadersResponse($headers)))
+ return($error);
+ }
+ switch($this->response_status)
+ {
+ case "301":
+ case "302":
+ case "303":
+ case "307":
+ if(strlen($error=$this->Redirect($headers)))
+ return($error);
+ break;
+ case "407":
+ if(strlen($error=$this->Authenticate($headers, 1, $proxy_authorization, $this->proxy_request_user, $this->proxy_request_password, $this->proxy_request_realm, $this->proxy_request_workstation)))
+ return($error);
+ if(strcmp($this->response_status,"401"))
+ return("");
+ case "401":
+ return($this->Authenticate($headers, 0, $proxy_authorization, $this->request_user, $this->request_password, $this->request_realm, $this->request_workstation));
+ }
+ return("");
+ }
+
+ Function ReadReplyBody(&$body,$length)
+ {
+ $body="";
+ if(strlen($this->error))
+ return($this->error);
+ switch($this->state)
+ {
+ case "Disconnected":
+ return($this->SetError("connection was not yet established", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "Connected":
+ case "ConnectedToProxy":
+ return($this->SetError("request was not sent", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ case "RequestSent":
+ if(($error=$this->ReadReplyHeaders($headers))!="")
+ return($error);
+ break;
+ case "GotReplyHeaders":
+ break;
+ case 'ResponseReceived':
+ $body = '';
+ return('');
+ default:
+ return($this->SetError("can not get request headers in the current connection state", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ }
+ if($this->content_length_set)
+ $length=min($this->content_length-$this->read_length,$length);
+ $body = '';
+ if($length>0)
+ {
+ if(!$this->EndOfInput()
+ && ($body=$this->ReadBytes($length))=="")
+ {
+ if(strlen($this->error))
+ return($this->SetError("could not get the request reply body: ".$this->error, $this->error_code));
+ }
+ $this->read_length+=strlen($body);
+ if($this->EndOfInput())
+ $this->state = 'ResponseReceived';
+ }
+ return("");
+ }
+
+ Function ReadWholeReplyBody(&$body)
+ {
+ $body = '';
+ for(;;)
+ {
+ if(strlen($error = $this->ReadReplyBody($block, $this->file_buffer_length)))
+ return($error);
+ if(strlen($block) == 0)
+ return('');
+ $body .= $block;
+ }
+ }
+
+ Function SaveCookies(&$cookies, $domain='', $secure_only=0, $persistent_only=0)
+ {
+ $now=gmdate("Y-m-d H-i-s");
+ $cookies=array();
+ for($secure_cookies=0,Reset($this->cookies);$secure_cookies<count($this->cookies);Next($this->cookies),$secure_cookies++)
+ {
+ $secure=Key($this->cookies);
+ if(!$secure_only
+ || $secure)
+ {
+ for($cookie_domain=0,Reset($this->cookies[$secure]);$cookie_domain<count($this->cookies[$secure]);Next($this->cookies[$secure]),$cookie_domain++)
+ {
+ $domain_pattern=Key($this->cookies[$secure]);
+ $match=strlen($domain)-strlen($domain_pattern);
+ if(strlen($domain)==0
+ || ($match>=0
+ && !strcmp($domain_pattern,substr($domain,$match))
+ && ($match==0
+ || $domain_pattern[0]=="."
+ || $domain[$match-1]==".")))
+ {
+ for(Reset($this->cookies[$secure][$domain_pattern]),$path_part=0;$path_part<count($this->cookies[$secure][$domain_pattern]);Next($this->cookies[$secure][$domain_pattern]),$path_part++)
+ {
+ $path=Key($this->cookies[$secure][$domain_pattern]);
+ for(Reset($this->cookies[$secure][$domain_pattern][$path]),$cookie=0;$cookie<count($this->cookies[$secure][$domain_pattern][$path]);Next($this->cookies[$secure][$domain_pattern][$path]),$cookie++)
+ {
+ $cookie_name=Key($this->cookies[$secure][$domain_pattern][$path]);
+ $expires=$this->cookies[$secure][$domain_pattern][$path][$cookie_name]["expires"];
+ if((!$persistent_only
+ && strlen($expires)==0)
+ || (strlen($expires)
+ && strcmp($now,$expires)<0))
+ $cookies[$secure][$domain_pattern][$path][$cookie_name]=$this->cookies[$secure][$domain_pattern][$path][$cookie_name];
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Function SavePersistentCookies(&$cookies, $domain='', $secure_only=0)
+ {
+ $this->SaveCookies($cookies, $domain, $secure_only, 1);
+ }
+
+ Function GetPersistentCookies(&$cookies, $domain='', $secure_only=0)
+ {
+ $this->SavePersistentCookies($cookies, $domain, $secure_only);
+ }
+
+ Function RestoreCookies($cookies, $clear=1)
+ {
+ $new_cookies=($clear ? array() : $this->cookies);
+ for($secure_cookies=0, Reset($cookies); $secure_cookies<count($cookies); Next($cookies), $secure_cookies++)
+ {
+ $secure=Key($cookies);
+ if(GetType($secure)!="integer")
+ return($this->SetError("invalid cookie secure value type (".serialize($secure).")", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ for($cookie_domain=0,Reset($cookies[$secure]);$cookie_domain<count($cookies[$secure]);Next($cookies[$secure]),$cookie_domain++)
+ {
+ $domain_pattern=Key($cookies[$secure]);
+ if(GetType($domain_pattern)!="string")
+ return($this->SetError("invalid cookie domain value type (".serialize($domain_pattern).")", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ for(Reset($cookies[$secure][$domain_pattern]),$path_part=0;$path_part<count($cookies[$secure][$domain_pattern]);Next($cookies[$secure][$domain_pattern]),$path_part++)
+ {
+ $path=Key($cookies[$secure][$domain_pattern]);
+ if(GetType($path)!="string"
+ || strcmp(substr($path, 0, 1), "/"))
+ return($this->SetError("invalid cookie path value type (".serialize($path).")", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ for(Reset($cookies[$secure][$domain_pattern][$path]),$cookie=0;$cookie<count($cookies[$secure][$domain_pattern][$path]);Next($cookies[$secure][$domain_pattern][$path]),$cookie++)
+ {
+ $cookie_name=Key($cookies[$secure][$domain_pattern][$path]);
+ $expires=$cookies[$secure][$domain_pattern][$path][$cookie_name]["expires"];
+ $value=$cookies[$secure][$domain_pattern][$path][$cookie_name]["value"];
+ if(GetType($expires)!="string"
+ || (strlen($expires)
+ && !preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\$/", $expires)))
+ return($this->SetError("invalid cookie expiry value type (".serialize($expires).")", HTTP_CLIENT_ERROR_INVALID_PARAMETERS));
+ $new_cookies[$secure][$domain_pattern][$path][$cookie_name]=array(
+ "name"=>$cookie_name,
+ "value"=>$value,
+ "domain"=>$domain_pattern,
+ "path"=>$path,
+ "expires"=>$expires,
+ "secure"=>$secure
+ );
+ }
+ }
+ }
+ }
+ $this->cookies=$new_cookies;
+ return("");
+ }
+};
+
+?>
\ No newline at end of file
--- /dev/null
+PHP OAuth API - Access API authorized by the users
+using the OAuth protocol
+
+This LICENSE is in the BSD license style.
+
+License Version Control:
+@(#) $Id: LICENSE,v 1.2 2013/02/11 05:17:53 mlemos Exp $
+
+Copyright (c) 2012-2013, Manuel Lemos
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ Neither the name of Manuel Lemos nor the names of his contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null
+<?php
+/*
+ * oauth_client.php
+ *
+ * @(#) $Id: oauth_client.php,v 1.93 2014/04/11 10:24:00 mlemos Exp $
+ *
+ */
+
+/*
+{metadocument}<?xml version="1.0" encoding="ISO-8859-1" ?>
+<class>
+
+ <package>net.manuellemos.oauth</package>
+
+ <version>@(#) $Id: oauth_client.php,v 1.93 2014/04/11 10:24:00 mlemos Exp $</version>
+ <copyright>Copyright � (C) Manuel Lemos 2012</copyright>
+ <title>OAuth client</title>
+ <author>Manuel Lemos</author>
+ <authoraddress>mlemos-at-acm.org</authoraddress>
+
+ <documentation>
+ <idiom>en</idiom>
+ <purpose>This class serves two main purposes:<paragraphbreak />
+ 1) Implement the OAuth protocol to retrieve a token from a server to
+ authorize the access to an API on behalf of the current
+ user.<paragraphbreak />
+ 2) Perform calls to a Web services API using a token previously
+ obtained using this class or a token provided some other way by the
+ Web services provider.</purpose>
+ <usage>Regardless of your purposes, you always need to start calling
+ the class <functionlink>Initialize</functionlink> function after
+ initializing setup variables. After you are done with the class,
+ always call the <functionlink>Finalize</functionlink> function at
+ the end.<paragraphbreak />
+ This class supports either OAuth protocol versions 1.0, 1.0a and
+ 2.0. It abstracts the differences between these protocol versions,
+ so the class usage is the same independently of the OAuth
+ version of the server.<paragraphbreak />
+ The class also provides built-in support to several popular OAuth
+ servers, so you do not have to manually configure all the details to
+ access those servers. Just set the
+ <variablelink>server</variablelink> variable to configure the class
+ to access one of the built-in supported servers.<paragraphbreak />
+ If you need to access one type of server that is not yet directly
+ supported by the class, you need to configure it explicitly setting
+ the variables: <variablelink>oauth_version</variablelink>,
+ <variablelink>url_parameters</variablelink>,
+ <variablelink>authorization_header</variablelink>,
+ <variablelink>request_token_url</variablelink>,
+ <variablelink>dialog_url</variablelink>,
+ <variablelink>offline_dialog_url</variablelink>,
+ <variablelink>append_state_to_redirect_uri</variablelink> and
+ <variablelink>access_token_url</variablelink>.<paragraphbreak />
+ Before proceeding to the actual OAuth authorization process, you
+ need to have registered your application with the OAuth server. The
+ registration provides you values to set the variables
+ <variablelink>client_id</variablelink> and
+ <variablelink>client_secret</variablelink>. Some servers also
+ provide an additional value to set the
+ <variablelink>api_key</variablelink> variable.<paragraphbreak />
+ You also need to set the variables
+ <variablelink>redirect_uri</variablelink> and
+ <variablelink>scope</variablelink> before calling the
+ <functionlink>Process</functionlink> function to make the class
+ perform the necessary interactions with the OAuth
+ server.<paragraphbreak />
+ The OAuth protocol involves multiple steps that include redirection
+ to the OAuth server. There it asks permission to the current user to
+ grant your application access to APIs on his/her behalf. When there
+ is a redirection, the class will set the
+ <variablelink>exit</variablelink> variable to
+ <booleanvalue>1</booleanvalue>. Then your script should exit
+ immediately without outputting anything.<paragraphbreak />
+ When the OAuth access token is successfully obtained, the following
+ variables are set by the class with the obtained values:
+ <variablelink>access_token</variablelink>,
+ <variablelink>access_token_secret</variablelink>,
+ <variablelink>access_token_expiry</variablelink>,
+ <variablelink>access_token_type</variablelink>. You may want to
+ store these values to use them later when calling the server
+ APIs.<paragraphbreak />
+ If there was a problem during OAuth authorization process, check the
+ variable <variablelink>authorization_error</variablelink> to
+ determine the reason.<paragraphbreak />
+ Once you get the access token, you can call the server APIs using
+ the <functionlink>CallAPI</functionlink> function. Check the
+ <variablelink>access_token_error</variablelink> variable to
+ determine if there was an error when trying to to call the
+ API.<paragraphbreak />
+ If for some reason the user has revoked the access to your
+ application, you need to ask the user to authorize your application
+ again. First you may need to call the function
+ <functionlink>ResetAccessToken</functionlink> to reset the value of
+ the access token that may be cached in session variables.</usage>
+ </documentation>
+
+{/metadocument}
+*/
+
+class oauth_client_class
+{
+/*
+{metadocument}
+ <variable>
+ <name>error</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Store the message that is returned when an error
+ occurs.</purpose>
+ <usage>Check this variable to understand what happened when a call to
+ any of the class functions has failed.<paragraphbreak />
+ This class uses cumulative error handling. This means that if one
+ class functions that may fail is called and this variable was
+ already set to an error message due to a failure in a previous call
+ to the same or other function, the function will also fail and does
+ not do anything.<paragraphbreak />
+ This allows programs using this class to safely call several
+ functions that may fail and only check the failure condition after
+ the last function call.<paragraphbreak />
+ Just set this variable to an empty string to clear the error
+ condition.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $error = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>debug</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Control whether debug output is enabled</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if you
+ need to check what is going on during calls to the class. When
+ enabled, the debug output goes either to the variable
+ <variablelink>debug_output</variablelink> and the PHP error log.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $debug = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>debug_http</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Control whether the dialog with the remote Web server
+ should also be logged.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if you
+ want to inspect the data exchange with the OAuth server.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $debug_http = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>exit</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Determine if the current script should be exited.</purpose>
+ <usage>Check this variable after calling the
+ <functionlink>Process</functionlink> function and exit your script
+ immediately if the variable is set to
+ <booleanvalue>1</booleanvalue>.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $exit = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>debug_output</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Capture the debug output generated by the class</purpose>
+ <usage>Inspect this variable if you need to see what happened during
+ the class function calls.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $debug_output = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>debug_prefix</name>
+ <type>STRING</type>
+ <value>OAuth client: </value>
+ <documentation>
+ <purpose>Mark the lines of the debug output to identify actions
+ performed by this class.</purpose>
+ <usage>Change this variable if you prefer the debug output lines to
+ be prefixed with a different text.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $debug_prefix = 'OAuth client: ';
+
+/*
+{metadocument}
+ <variable>
+ <name>server</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Identify the type of OAuth server to access.</purpose>
+ <usage>The class provides built-in support to several types of OAuth
+ servers. This means that the class can automatically initialize
+ several configuration variables just by setting this server
+ variable.<paragraphbreak />
+ Currently it supports the following servers:
+ <stringvalue>37Signals</stringvalue>,
+ <stringvalue>Amazon</stringvalue>,
+ <stringvalue>Bitbucket</stringvalue>,
+ <stringvalue>Box</stringvalue>,
+ <stringvalue>Buffer</stringvalue>,
+ <stringvalue>Discogs</stringvalue>,
+ <stringvalue>Disqus</stringvalue>,
+ <stringvalue>Dropbox</stringvalue> (Dropbox with OAuth 1.0),
+ <stringvalue>Dropbox2</stringvalue> (Dropbox with OAuth 2.0),
+ <stringvalue>Etsy</stringvalue>,
+ <stringvalue>Eventful</stringvalue>,
+ <stringvalue>Facebook</stringvalue>,
+ <stringvalue>Fitbit</stringvalue>,
+ <stringvalue>Flickr</stringvalue>,
+ <stringvalue>Foursquare</stringvalue>,
+ <stringvalue>github</stringvalue>,
+ <stringvalue>Google</stringvalue>,
+ <stringvalue>Google1</stringvalue> (Google with OAuth 1.0),
+ <stringvalue>Instagram</stringvalue>,
+ <stringvalue>LinkedIn</stringvalue>,
+ <stringvalue>Microsoft</stringvalue>,
+ <stringvalue>Rdio</stringvalue>,
+ <stringvalue>Reddit</stringvalue>,
+ <stringvalue>Salesforce</stringvalue>,
+ <stringvalue>Scoop.it</stringvalue>,
+ <stringvalue>StockTwits</stringvalue>,
+ <stringvalue>SurveyMonkey</stringvalue>,
+ <stringvalue>Tumblr</stringvalue>,
+ <stringvalue>Twitter</stringvalue>,
+ <stringvalue>Vimeo</stringvalue>,
+ <stringvalue>VK</stringvalue>,
+ <stringvalue>Withings</stringvalue>,
+ <stringvalue>Xero</stringvalue>,
+ <stringvalue>XING</stringvalue> and
+ <stringvalue>Yahoo</stringvalue>. Please contact the author if you
+ would like to ask to add built-in support for other types of OAuth
+ servers.<paragraphbreak />
+ If you want to access other types of OAuth servers that are not
+ yet supported, set this variable to an empty string and configure
+ other variables with values specific to those servers.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $server = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>configuration_file</name>
+ <type>STRING</type>
+ <value>oauth_configuration.json</value>
+ <documentation>
+ <purpose>Specify the path of the configuration file that defines the
+ properties of additional OAuth server types.</purpose>
+ <usage>Change the path in this variable if you are accessing a type
+ of server without support built-in the class and you need to put
+ the configuration file path in a different directory.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $configuration_file = 'oauth_configuration.json';
+
+/*
+{metadocument}
+ <variable>
+ <name>request_token_url</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>URL of the OAuth server to request the initial token for
+ OAuth 1.0 and 1.0a servers.</purpose>
+ <usage>Set this variable to the OAuth request token URL when you are
+ not accessing one of the built-in supported OAuth
+ servers.<paragraphbreak />
+ For OAuth 1.0 and 1.0a servers, the request token URL can have
+ certain marks that will act as template placeholders which will be
+ replaced with given values before requesting the authorization
+ token. Currently it supports the following placeholder
+ marks:<paragraphbreak />
+ {SCOPE} - scope of the requested permissions to the granted by the
+ OAuth server with the user permissions</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $request_token_url = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>dialog_url</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>URL of the OAuth server to redirect the browser so the user
+ can grant access to your application.</purpose>
+ <usage>Set this variable to the OAuth request token URL when you are
+ not accessing one of the built-in supported OAuth servers.<paragraphbreak />
+ For OAuth 1.0a servers that return the login dialog URL
+ automatically, set this variable to
+ <stringvalue>automatic</stringvalue><paragraphbreak />
+ For certain servers, the dialog URL can have certain marks that
+ will act as template placeholders which will be replaced with
+ values defined before redirecting the users browser. Currently it
+ supports the following placeholder marks:<paragraphbreak />
+ {REDIRECT_URI} - URL to redirect when returning from the OAuth
+ server authorization page<paragraphbreak />
+ {CLIENT_ID} - client application identifier registered at the
+ server<paragraphbreak />
+ {SCOPE} - scope of the requested permissions to the granted by the
+ OAuth server with the user permissions<paragraphbreak />
+ {STATE} - identifier of the OAuth session state<paragraphbreak />
+ {API_KEY} - API key to access the server</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $dialog_url = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>offline_dialog_url</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>URL of the OAuth server to redirect the browser so the user
+ can grant access to your application when offline access is
+ requested.</purpose>
+ <usage>Set this variable to the OAuth request token URL when you are
+ not accessing one of the built-in supported OAuth servers and the
+ OAuth server supports offline access.<paragraphbreak />
+ It should have the same format as the
+ <variablelink>dialog_url</variablelink> variable.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $offline_dialog_url = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>append_state_to_redirect_uri</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Pass the OAuth session state in a variable with a different
+ name to work around implementation bugs of certain OAuth
+ servers</purpose>
+ <usage>Set this variable when you are not accessing one of the
+ built-in supported OAuth servers if the OAuth server has a bug
+ that makes it not pass back the OAuth state identifier in a
+ request variable named state.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $append_state_to_redirect_uri = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_url</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>OAuth server URL that will return the access token
+ URL.</purpose>
+ <usage>Set this variable to the OAuth access token URL when you are
+ not accessing one of the built-in supported OAuth servers.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_url = '';
+
+
+/*
+{metadocument}
+ <variable>
+ <name>oauth_version</name>
+ <type>STRING</type>
+ <value>2.0</value>
+ <documentation>
+ <purpose>Version of the protocol version supported by the OAuth
+ server.</purpose>
+ <usage>Set this variable to the OAuth server protocol version when
+ you are not accessing one of the built-in supported OAuth
+ servers.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $oauth_version = '2.0';
+
+/*
+{metadocument}
+ <variable>
+ <name>url_parameters</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Determine if the API call parameters should be moved to the
+ call URL.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if the
+ API you need to call requires that the call parameters always be
+ passed via the API URL.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $url_parameters = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>authorization_header</name>
+ <type>BOOLEAN</type>
+ <value>1</value>
+ <documentation>
+ <purpose>Determine if the OAuth parameters should be passed via HTTP
+ Authorization request header.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if the
+ OAuth server requires that the OAuth parameters be passed using
+ the HTTP Authorization instead of the request URI parameters.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $authorization_header = true;
+
+/*
+{metadocument}
+ <variable>
+ <name>token_request_method</name>
+ <type>STRING</type>
+ <value>GET</value>
+ <documentation>
+ <purpose>Define the HTTP method that should be used to request
+ tokens from the server.</purpose>
+ <usage>Set this variable to <stringvalue>POST</stringvalue> if the
+ OAuth server does not support requesting tokens using the HTTP GET
+ method.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $token_request_method = 'GET';
+
+/*
+{metadocument}
+ <variable>
+ <name>signature_method</name>
+ <type>STRING</type>
+ <value>HMAC-SHA1</value>
+ <documentation>
+ <purpose>Define the method to generate the signature for API request
+ parameters values.</purpose>
+ <usage>Currently it supports <stringvalue>PLAINTEXT</stringvalue>
+ and <stringvalue>HMAC-SHA1</stringvalue>.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $signature_method = 'HMAC-SHA1';
+
+/*
+{metadocument}
+ <variable>
+ <name>redirect_uri</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>URL of the current script page that is calling this
+ class</purpose>
+ <usage>Set this variable to the current script page URL before
+ proceeding the the OAuth authorization process.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $redirect_uri = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>client_id</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Identifier of your application registered with the OAuth
+ server</purpose>
+ <usage>Set this variable to the application identifier that is
+ provided by the OAuth server when you register the
+ application.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $client_id = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>client_secret</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Secret value assigned to your application when it is
+ registered with the OAuth server.</purpose>
+ <usage>Set this variable to the application secret that is provided
+ by the OAuth server when you register the application.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $client_secret = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>api_key</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Identifier of your API key provided by the OAuth
+ server</purpose>
+ <usage>Set this variable to the API key if the OAuth server requires
+ one.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $api_key = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>get_token_with_api_key</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Option to determine if the access token should be retrieved
+ using the API key value instead of the client secret.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if the
+ OAuth server requires that the client secret be set to the API key
+ when retrieving the OAuth token.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $get_token_with_api_key = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>scope</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Permissions that your application needs to call the OAuth
+ server APIs</purpose>
+ <usage>Check the documentation of the APIs that your application
+ needs to call to set this variable with the identifiers of the
+ permissions that the user needs to grant to your application.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $scope = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>offline</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Specify whether it will be necessary to call the API when
+ the user is not present and the server supports renewing expired
+ access tokens using refresh tokens.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if the
+ server supports renewing expired tokens automatically when the
+ user is not present.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $offline = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Access token obtained from the OAuth server</purpose>
+ <usage>Check this variable to get the obtained access token upon
+ successful OAuth authorization.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_secret</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Access token secret obtained from the OAuth server</purpose>
+ <usage>If the OAuth protocol version is 1.0 or 1.0a, check this
+ variable to get the obtained access token secret upon successful
+ OAuth authorization.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_secret = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_expiry</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Timestamp of the expiry of the access token obtained from
+ the OAuth server.</purpose>
+ <usage>Check this variable to get the obtained access token expiry
+ time upon successful OAuth authorization. If this variable is
+ empty, that means no expiry time was set.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_expiry = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_type</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Type of access token obtained from the OAuth server.</purpose>
+ <usage>Check this variable to get the obtained access token type
+ upon successful OAuth authorization.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_type = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>default_access_token_type</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Type of access token to be assumed when the OAuth server
+ does not specify an access token type.</purpose>
+ <usage>Set this variable if the server requires a certain type of
+ access token to be used but it does not specify a token type
+ when the access token is returned.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $default_access_token_type = '';
+
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_parameter</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Name of the access token parameter to be passed in API call
+ requests.</purpose>
+ <usage>Set this variable to a non-empty string to override the
+ default name for the access token parameter which is
+ <stringvalue>oauth_token</stringvalue> of OAuth 1 and
+ <stringvalue>access_token</stringvalue> for OAuth 2.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_parameter = '';
+
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_response</name>
+ <type>ARRAY</type>
+ <documentation>
+ <purpose>The original response for the access token request</purpose>
+ <usage>Check this variable if the OAuth server returns custom
+ parameters in the request to obtain the access token.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_response;
+
+/*
+{metadocument}
+ <variable>
+ <name>store_access_token_response</name>
+ <type>BOOLEAN</type>
+ <value>0</value>
+ <documentation>
+ <purpose>Option to determine if the original response for the access
+ token request should be stored in the
+ <variablelink>access_token_response</variablelink>
+ variable.</purpose>
+ <usage>Set this variable to <booleanvalue>1</booleanvalue> if the
+ OAuth server returns custom parameters in the request to obtain
+ the access token that may be needed in subsequent API calls.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $store_access_token_response = false;
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_authentication</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Option to determine if the requests to obtain a new access
+ token should use authentication to pass the application client ID
+ and secret.</purpose>
+ <usage>Set this variable to <stringvalue>basic</stringvalue> if the
+ OAuth server requires that the the client ID and secret be passed
+ using HTTP basic authentication headers when retrieving a new
+ token.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_authentication = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>refresh_token</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Refresh token obtained from the OAuth server</purpose>
+ <usage>Check this variable to get the obtained refresh token upon
+ successful OAuth authorization.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $refresh_token = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>access_token_error</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Error message returned when a call to the API fails.</purpose>
+ <usage>Check this variable to determine if there was an error while
+ calling the Web services API when using the
+ <functionlink>CallAPI</functionlink> function.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $access_token_error = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>authorization_error</name>
+ <type>STRING</type>
+ <value></value>
+ <documentation>
+ <purpose>Error message returned when it was not possible to obtain
+ an OAuth access token</purpose>
+ <usage>Check this variable to determine if there was an error while
+ trying to obtain the OAuth access token.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $authorization_error = '';
+
+/*
+{metadocument}
+ <variable>
+ <name>response_status</name>
+ <type>INTEGER</type>
+ <value>0</value>
+ <documentation>
+ <purpose>HTTP response status returned by the server when calling an
+ API</purpose>
+ <usage>Check this variable after calling the
+ <functionlink>CallAPI</functionlink> function if the API calls and you
+ need to process the error depending the response status.
+ <integervalue>200</integervalue> means no error.
+ <integervalue>0</integervalue> means the server response was not
+ retrieved.</usage>
+ </documentation>
+ </variable>
+{/metadocument}
+*/
+ var $response_status = 0;
+
+ var $oauth_user_agent = 'PHP-OAuth-API (http://www.phpclasses.org/oauth-api $Revision: 1.93 $)';
+
+ Function SetError($error)
+ {
+ $this->error = $error;
+ if($this->debug)
+ $this->OutputDebug('Error: '.$error);
+ return(false);
+ }
+
+ Function SetPHPError($error, &$php_error_message)
+ {
+ if(IsSet($php_error_message)
+ && strlen($php_error_message))
+ $error.=": ".$php_error_message;
+ return($this->SetError($error));
+ }
+
+ Function OutputDebug($message)
+ {
+ if($this->debug)
+ {
+ $message = $this->debug_prefix.$message;
+ $this->debug_output .= $message."\n";;
+ error_log($message);
+ }
+ return(true);
+ }
+
+ Function GetRequestTokenURL(&$request_token_url)
+ {
+ $request_token_url = $this->request_token_url;
+ return(true);
+ }
+
+ Function GetDialogURL(&$url, $redirect_uri = '', $state = '')
+ {
+ $url = (($this->offline && strlen($this->offline_dialog_url)) ? $this->offline_dialog_url : $this->dialog_url);
+ if(strlen($url) === 0)
+ return $this->SetError('the dialog URL '.($this->offline ? 'for offline access ' : '').'is not defined for this server');
+ $url = str_replace(
+ '{REDIRECT_URI}', UrlEncode($redirect_uri), str_replace(
+ '{STATE}', UrlEncode($state), str_replace(
+ '{CLIENT_ID}', UrlEncode($this->client_id), str_replace(
+ '{API_KEY}', UrlEncode($this->api_key), str_replace(
+ '{SCOPE}', UrlEncode($this->scope),
+ $url)))));
+ return(true);
+ }
+
+ Function GetAccessTokenURL(&$access_token_url)
+ {
+ $access_token_url = str_replace('{API_KEY}', $this->api_key, $this->access_token_url);
+ return(true);
+ }
+
+ Function GetStoredState(&$state)
+ {
+ if(!function_exists('session_start'))
+ return $this->SetError('Session variables are not accessible in this PHP environment');
+ if(session_id() === ''
+ && !session_start())
+ return($this->SetPHPError('it was not possible to start the PHP session', $php_errormsg));
+ if(IsSet($_SESSION['OAUTH_STATE']))
+ $state = $_SESSION['OAUTH_STATE'];
+ else
+ $state = $_SESSION['OAUTH_STATE'] = time().'-'.substr(md5(rand().time()), 0, 6);
+ return(true);
+ }
+
+ Function GetRequestState(&$state)
+ {
+ $check = (strlen($this->append_state_to_redirect_uri) ? $this->append_state_to_redirect_uri : 'state');
+ $state = (IsSet($_GET[$check]) ? $_GET[$check] : null);
+ return(true);
+ }
+
+ Function GetRequestCode(&$code)
+ {
+ $code = (IsSet($_GET['code']) ? $_GET['code'] : null);
+ return(true);
+ }
+
+ Function GetRequestError(&$error)
+ {
+ $error = (IsSet($_GET['error']) ? $_GET['error'] : null);
+ return(true);
+ }
+
+ Function GetRequestDenied(&$denied)
+ {
+ $denied = (IsSet($_GET['denied']) ? $_GET['denied'] : null);
+ return(true);
+ }
+
+ Function GetRequestToken(&$token, &$verifier)
+ {
+ $token = (IsSet($_GET['oauth_token']) ? $_GET['oauth_token'] : null);
+ $verifier = (IsSet($_GET['oauth_verifier']) ? $_GET['oauth_verifier'] : null);
+ return(true);
+ }
+
+ Function GetRedirectURI(&$redirect_uri)
+ {
+ if(strlen($this->redirect_uri))
+ $redirect_uri = $this->redirect_uri;
+ else
+ $redirect_uri = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
+ return true;
+ }
+
+/*
+{metadocument}
+ <function>
+ <name>Redirect</name>
+ <type>VOID</type>
+ <documentation>
+ <purpose>Redirect the user browser to a given page.</purpose>
+ <usage>This function is meant to be only be called from inside the
+ class. By default it issues HTTP 302 response status and sets the
+ redirection location to a given URL. Sub-classes may override this
+ function to implement a different way to redirect the user
+ browser.</usage>
+ </documentation>
+ <argument>
+ <name>url</name>
+ <type>STRING</type>
+ <documentation>
+ <purpose>String with the full URL of the page to redirect.</purpose>
+ </documentation>
+ </argument>
+ <do>
+{/metadocument}
+*/
+ Function Redirect($url)
+ {
+ Header('HTTP/1.0 302 OAuth Redirection');
+ Header('Location: '.$url);
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>StoreAccessToken</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Store the values of the access token when it is succefully
+ retrieved from the OAuth server.</purpose>
+ <usage>This function is meant to be only be called from inside the
+ class. By default it stores access tokens in a session variable
+ named <stringvalue>OAUTH_ACCESS_TOKEN</stringvalue>.<paragraphbreak />
+ Actual implementations should create a sub-class and override this
+ function to make the access token values be stored in other types
+ of containers, like for instance databases.</usage>
+ <returnvalue>This function should return
+ <booleanvalue>1</booleanvalue> if the access token was stored
+ successfully.</returnvalue>
+ </documentation>
+ <argument>
+ <name>access_token</name>
+ <type>HASH</type>
+ <documentation>
+ <purpose>Associative array with properties of the access token.
+ The array may have set the following
+ properties:<paragraphbreak />
+ <stringvalue>value</stringvalue>: string value of the access
+ token<paragraphbreak />
+ <stringvalue>authorized</stringvalue>: boolean value that
+ determines if the access token was obtained
+ successfully<paragraphbreak />
+ <stringvalue>expiry</stringvalue>: (optional) timestamp in ISO
+ format relative to UTC time zone of the access token expiry
+ time<paragraphbreak />
+ <stringvalue>type</stringvalue>: (optional) type of OAuth token
+ that may determine how it should be used when sending API call
+ requests.<paragraphbreak />
+ <stringvalue>refresh</stringvalue>: (optional) token that some
+ servers may set to allowing refreshing access tokens when they
+ expire.</purpose>
+ </documentation>
+ </argument>
+ <do>
+{/metadocument}
+*/
+ Function StoreAccessToken($access_token)
+ {
+ if(!function_exists('session_start'))
+ return $this->SetError('Session variables are not accessible in this PHP environment');
+ if(session_id() === ''
+ && !session_start())
+ return($this->SetPHPError('it was not possible to start the PHP session', $php_errormsg));
+ if(!$this->GetAccessTokenURL($access_token_url))
+ return false;
+ $_SESSION['OAUTH_ACCESS_TOKEN'][$access_token_url] = $access_token;
+ return true;
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>GetAccessToken</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Retrieve the OAuth access token if it was already
+ previously stored by the
+ <functionlink>StoreAccessToken</functionlink> function.</purpose>
+ <usage>This function is meant to be only be called from inside the
+ class. By default it retrieves access tokens stored in a session
+ variable named
+ <stringvalue>OAUTH_ACCESS_TOKEN</stringvalue>.<paragraphbreak />
+ Actual implementations should create a sub-class and override this
+ function to retrieve the access token values from other types of
+ containers, like for instance databases.</usage>
+ <returnvalue>This function should return
+ <booleanvalue>1</booleanvalue> if the access token was retrieved
+ successfully.</returnvalue>
+ </documentation>
+ <argument>
+ <name>access_token</name>
+ <type>STRING</type>
+ <out />
+ <documentation>
+ <purpose>Return the properties of the access token in an
+ associative array. If the access token was not yet stored, it
+ returns an empty array. Otherwise, the properties it may return
+ are the same that may be passed to the
+ <functionlink>StoreAccessToken</functionlink>.</purpose>
+ </documentation>
+ </argument>
+ <do>
+{/metadocument}
+*/
+ Function GetAccessToken(&$access_token)
+ {
+ if(!function_exists('session_start'))
+ return $this->SetError('Session variables are not accessible in this PHP environment');
+ if(session_id() === ''
+ && !session_start())
+ return($this->SetPHPError('it was not possible to start the PHP session', $php_errormsg));
+ if(!$this->GetAccessTokenURL($access_token_url))
+ return false;
+ if(IsSet($_SESSION['OAUTH_ACCESS_TOKEN'][$access_token_url]))
+ $access_token = $_SESSION['OAUTH_ACCESS_TOKEN'][$access_token_url];
+ else
+ $access_token = array();
+ return true;
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>ResetAccessToken</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Reset the access token to a state back when the user has
+ not yet authorized the access to the OAuth server API.</purpose>
+ <usage>Call this function if for some reason the token to access
+ the API was revoked and you need to ask the user to authorize
+ the access again.<paragraphbreak />
+ By default the class stores and retrieves access tokens in a
+ session variable named
+ <stringvalue>OAUTH_ACCESS_TOKEN</stringvalue>.<paragraphbreak />
+ This function must be called when the user is accessing your site
+ pages, so it can reset the information stored in session variables
+ that cache the state of a previously retrieved access
+ token.<paragraphbreak />
+ Actual implementations should create a sub-class and override this
+ function to reset the access token state when it is stored in
+ other types of containers, like for instance databases.</usage>
+ <returnvalue>This function should return
+ <booleanvalue>1</booleanvalue> if the access token was resetted
+ successfully.</returnvalue>
+ </documentation>
+ <do>
+{/metadocument}
+*/
+ Function ResetAccessToken()
+ {
+ if(!$this->GetAccessTokenURL($access_token_url))
+ return false;
+ if($this->debug)
+ $this->OutputDebug('Resetting the access token status for OAuth server located at '.$access_token_url);
+ if(!function_exists('session_start'))
+ return $this->SetError('Session variables are not accessible in this PHP environment');
+ if(session_id() === ''
+ && !session_start())
+ return($this->SetPHPError('it was not possible to start the PHP session', $php_errormsg));
+ if(IsSet($_SESSION['OAUTH_ACCESS_TOKEN'][$access_token_url]))
+ Unset($_SESSION['OAUTH_ACCESS_TOKEN'][$access_token_url]);
+ return true;
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+ Function Encode($value)
+ {
+ return(is_array($value) ? $this->EncodeArray($value) : str_replace('%7E', '~', str_replace('+',' ', RawURLEncode($value))));
+ }
+
+ Function EncodeArray($array)
+ {
+ foreach($array as $key => $value)
+ $array[$key] = $this->Encode($value);
+ return $array;
+ }
+
+ Function HMAC($function, $data, $key)
+ {
+ switch($function)
+ {
+ case 'sha1':
+ $pack = 'H40';
+ break;
+ default:
+ if($this->debug)
+ $this->OutputDebug($function.' is not a supported an HMAC hash type');
+ return('');
+ }
+ if(strlen($key) > 64)
+ $key = pack($pack, $function($key));
+ if(strlen($key) < 64)
+ $key = str_pad($key, 64, "\0");
+ return(pack($pack, $function((str_repeat("\x5c", 64) ^ $key).pack($pack, $function((str_repeat("\x36", 64) ^ $key).$data)))));
+ }
+
+ Function SendAPIRequest($url, $method, $parameters, $oauth, $options, &$response)
+ {
+ $this->response_status = 0;
+ $http = new http_class;
+ $http->debug = ($this->debug && $this->debug_http);
+ $http->log_debug = true;
+ $http->sasl_authenticate = 0;
+ $http->user_agent = $this->oauth_user_agent;
+ $http->redirection_limit = (IsSet($options['FollowRedirection']) ? intval($options['FollowRedirection']) : 0);
+ $http->follow_redirect = ($http->redirection_limit != 0);
+ if($this->debug)
+ $this->OutputDebug('Accessing the '.$options['Resource'].' at '.$url);
+ $post_files = array();
+ $method = strtoupper($method);
+ $authorization = '';
+ $type = (IsSet($options['RequestContentType']) ? strtolower(trim(strtok($options['RequestContentType'], ';'))) : (($method === 'POST' || IsSet($oauth)) ? 'application/x-www-form-urlencoded' : ''));
+ if(IsSet($oauth))
+ {
+ $values = array(
+ 'oauth_consumer_key'=>$this->client_id,
+ 'oauth_nonce'=>md5(uniqid(rand(), true)),
+ 'oauth_signature_method'=>$this->signature_method,
+ 'oauth_timestamp'=>time(),
+ 'oauth_version'=>'1.0',
+ );
+ $files = (IsSet($options['Files']) ? $options['Files'] : array());
+ if(count($files))
+ {
+ foreach($files as $name => $value)
+ {
+ if(!IsSet($parameters[$name]))
+ return($this->SetError('it was specified an file parameters named '.$name));
+ $file = array();
+ switch(IsSet($value['Type']) ? $value['Type'] : 'FileName')
+ {
+ case 'FileName':
+ $file['FileName'] = $parameters[$name];
+ break;
+ case 'Data':
+ $file['Data'] = $parameters[$name];
+ break;
+ default:
+ return($this->SetError($value['Type'].' is not a valid type for file '.$name));
+ }
+ $file['ContentType'] = (IsSet($value['Content-Type']) ? $value['Content-Type'] : 'automatic/name');
+ $post_files[$name] = $file;
+ }
+ UnSet($parameters[$name]);
+ if($method !== 'POST')
+ {
+ $this->OutputDebug('For uploading files the method should be POST not '.$method);
+ $method = 'POST';
+ }
+ if($type !== 'multipart/form-data')
+ {
+ if(IsSet($options['RequestContentType']))
+ return($this->SetError('the request content type for uploading files should be multipart/form-data'));
+ $type = 'multipart/form-data';
+ }
+ $value_parameters = array();
+ }
+ else
+ {
+ if($this->url_parameters
+ && $type === 'application/x-www-form-urlencoded'
+ && count($parameters))
+ {
+ $first = (strpos($url, '?') === false);
+ foreach($parameters as $parameter => $value)
+ {
+ $url .= ($first ? '?' : '&').UrlEncode($parameter).'='.UrlEncode($value);
+ $first = false;
+ }
+ $parameters = array();
+ }
+ $value_parameters = (($type !== 'application/x-www-form-urlencoded') ? array() : $parameters);
+ }
+ $header_values = ($method === 'GET' ? array_merge($values, $oauth, $value_parameters) : array_merge($values, $oauth));
+ $values = array_merge($values, $oauth, $value_parameters);
+ $key = $this->Encode($this->client_secret).'&'.$this->Encode($this->access_token_secret);
+ switch($this->signature_method)
+ {
+ case 'PLAINTEXT':
+ $values['oauth_signature'] = $key;
+ break;
+ case 'HMAC-SHA1':
+ $uri = strtok($url, '?');
+ $sign = $method.'&'.$this->Encode($uri).'&';
+ $first = true;
+ $sign_values = $values;
+ $u = parse_url($url);
+ if(IsSet($u['query']))
+ {
+ parse_str($u['query'], $q);
+ foreach($q as $parameter => $value)
+ $sign_values[$parameter] = $value;
+ }
+ KSort($sign_values);
+ foreach($sign_values as $parameter => $value)
+ {
+ $sign .= $this->Encode(($first ? '' : '&').$parameter.'='.$this->Encode($value));
+ $first = false;
+ }
+ $header_values['oauth_signature'] = $values['oauth_signature'] = base64_encode($this->HMAC('sha1', $sign, $key));
+ break;
+ default:
+ return $this->SetError($this->signature_method.' signature method is not yet supported');
+ }
+ if($this->authorization_header)
+ {
+ $authorization = 'OAuth';
+ $first = true;
+ foreach($header_values as $parameter => $value)
+ {
+ $authorization .= ($first ? ' ' : ',').$parameter.'="'.$this->Encode($value).'"';
+ $first = false;
+ }
+ $post_values = $parameters;
+ }
+ else
+ {
+ if($method === 'GET'
+ || (IsSet($options['PostValuesInURI'])
+ && $options['PostValuesInURI']))
+ {
+ $first = (strcspn($url, '?') == strlen($url));
+ foreach($values as $parameter => $value)
+ {
+ $url .= ($first ? '?' : '&').$parameter.'='.$this->Encode($value);
+ $first = false;
+ }
+ $post_values = array();
+ }
+ else
+ $post_values = $values;
+ }
+ }
+ else
+ {
+ $post_values = $parameters;
+ if(count($parameters))
+ {
+ switch($type)
+ {
+ case 'application/x-www-form-urlencoded':
+ case 'multipart/form-data':
+ case 'application/json':
+ break;
+ default:
+ $first = (strpos($url, '?') === false);
+ foreach($parameters as $name => $value)
+ {
+ if(GetType($value) === 'array')
+ {
+ foreach($value as $index => $value)
+ {
+ $url .= ($first ? '?' : '&').$name.'='.UrlEncode($value);
+ $first = false;
+ }
+ }
+ else
+ {
+ $url .= ($first ? '?' : '&').$name.'='.UrlEncode($value);
+ $first = false;
+ }
+ }
+ }
+ }
+ }
+ if(strlen($authorization) === 0
+ && !strcasecmp($this->access_token_type, 'Bearer'))
+ $authorization = 'Bearer '.$this->access_token;
+ if(strlen($error = $http->GetRequestArguments($url, $arguments)))
+ return($this->SetError('it was not possible to open the '.$options['Resource'].' URL: '.$error));
+ if(strlen($error = $http->Open($arguments)))
+ return($this->SetError('it was not possible to open the '.$options['Resource'].' URL: '.$error));
+ if(count($post_files))
+ $arguments['PostFiles'] = $post_files;
+ $arguments['RequestMethod'] = $method;
+ switch($type)
+ {
+ case 'application/x-www-form-urlencoded':
+ case 'multipart/form-data':
+ if(IsSet($options['RequestBody']))
+ return($this->SetError('the request body is defined automatically from the parameters'));
+ $arguments['PostValues'] = $post_values;
+ break;
+ case 'application/json':
+ $arguments['Headers']['Content-Type'] = $options['RequestContentType'];
+ if(!IsSet($options['RequestBody']))
+ {
+ $arguments['Body'] = json_encode($parameters);
+ break;
+ }
+ if(!IsSet($options['RequestBody']))
+ return($this->SetError('it was not specified the body value of the of the API call request'));
+ $arguments['Headers']['Content-Type'] = $options['RequestContentType'];
+ $arguments['Body'] = $options['RequestBody'];
+ break;
+ }
+ $arguments['Headers']['Accept'] = (IsSet($options['Accept']) ? $options['Accept'] : '*/*');
+ switch(IsSet($options['AccessTokenAuthentication']) ? strtolower($options['AccessTokenAuthentication']) : '')
+ {
+ case 'basic':
+ $arguments['Headers']['Authorization'] = 'Basic '.base64_encode($this->client_id.':'.($this->get_token_with_api_key ? $this->api_key : $this->client_secret));
+ break;
+ case '':
+ if(strlen($authorization))
+ $arguments['Headers']['Authorization'] = $authorization;
+ break;
+ default:
+ return($this->SetError($this->access_token_authentication.' is not a supported authentication mechanism to retrieve an access token'));
+ }
+ if(strlen($error = $http->SendRequest($arguments))
+ || strlen($error = $http->ReadReplyHeaders($headers)))
+ {
+ $http->Close();
+ return($this->SetError('it was not possible to retrieve the '.$options['Resource'].': '.$error));
+ }
+ $error = $http->ReadWholeReplyBody($data);
+ $http->Close();
+ if(strlen($error))
+ {
+ return($this->SetError('it was not possible to access the '.$options['Resource'].': '.$error));
+ }
+ $this->response_status = intval($http->response_status);
+ $content_type = (IsSet($options['ResponseContentType']) ? $options['ResponseContentType'] : (IsSet($headers['content-type']) ? strtolower(trim(strtok($headers['content-type'], ';'))) : 'unspecified'));
+ $content_type = preg_replace('/^(.+\\/).+\\+(.+)$/', '\\1\\2', $content_type);
+ switch($content_type)
+ {
+ case 'text/javascript':
+ case 'application/json':
+ if(!function_exists('json_decode'))
+ return($this->SetError('the JSON extension is not available in this PHP setup'));
+ $object = json_decode($data);
+ switch(GetType($object))
+ {
+ case 'object':
+ if(!IsSet($options['ConvertObjects'])
+ || !$options['ConvertObjects'])
+ $response = $object;
+ else
+ {
+ $response = array();
+ foreach($object as $property => $value)
+ $response[$property] = $value;
+ }
+ break;
+ case 'array':
+ $response = $object;
+ break;
+ default:
+ if(!IsSet($object))
+ return($this->SetError('it was not returned a valid JSON definition of the '.$options['Resource'].' values'));
+ $response = $object;
+ break;
+ }
+ break;
+ case 'application/x-www-form-urlencoded':
+ case 'text/plain':
+ case 'text/html':
+ parse_str($data, $response);
+ break;
+ case 'text/xml':
+ if(IsSet($options['DecodeXMLResponse']))
+ {
+ switch(strtolower($options['DecodeXMLResponse']))
+ {
+ case 'simplexml':
+ if($this->debug)
+ $this->OutputDebug('Decoding XML response with simplexml');
+ try
+ {
+ $response = @new SimpleXMLElement($data);
+ }
+ catch(Exception $exception)
+ {
+ return $this->SetError('Could not parse XML response: '.$exception->getMessage());
+ }
+ break 2;
+ default:
+ return $this->SetError($options['DecodeXML'].' is not a supported method to decode XML responses');
+ }
+ }
+ default:
+ $response = $data;
+ break;
+ }
+ if($this->response_status >= 200
+ && $this->response_status < 300)
+ $this->access_token_error = '';
+ else
+ {
+ $this->access_token_error = 'it was not possible to access the '.$options['Resource'].': it was returned an unexpected response status '.$http->response_status.' Response: '.$data;
+ if($this->debug)
+ $this->OutputDebug('Could not retrieve the OAuth access token. Error: '.$this->access_token_error);
+ if(IsSet($options['FailOnAccessError'])
+ && $options['FailOnAccessError'])
+ {
+ $this->error = $this->access_token_error;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ Function ProcessToken($code, $refresh)
+ {
+ if(!$this->GetRedirectURI($redirect_uri))
+ return false;
+ if($refresh)
+ {
+ $values = array(
+ 'refresh_token'=>$this->refresh_token,
+ 'grant_type'=>'refresh_token',
+ 'scope'=>$this->scope,
+ );
+ }
+ else
+ {
+ $values = array(
+ 'code'=>$code,
+ 'redirect_uri'=>$redirect_uri,
+ 'grant_type'=>'authorization_code'
+ );
+ }
+ $options = array(
+ 'Resource'=>'OAuth '.($refresh ? 'refresh' : 'access').' token',
+ 'ConvertObjects'=>true
+ );
+ switch(strtolower($this->access_token_authentication))
+ {
+ case 'basic':
+ $options['AccessTokenAuthentication'] = $this->access_token_authentication;
+ $values['redirect_uri'] = $redirect_uri;
+ break;
+ case '':
+ $values['client_id'] = $this->client_id;
+ $values['client_secret'] = ($this->get_token_with_api_key ? $this->api_key : $this->client_secret);
+ break;
+ default:
+ return($this->SetError($this->access_token_authentication.' is not a supported authentication mechanism to retrieve an access token'));
+ }
+ if(!$this->GetAccessTokenURL($access_token_url))
+ return false;
+ if(!$this->SendAPIRequest($access_token_url, 'POST', $values, null, $options, $response))
+ return false;
+ if(strlen($this->access_token_error))
+ {
+ $this->authorization_error = $this->access_token_error;
+ return true;
+ }
+ if(!IsSet($response['access_token']))
+ {
+ if(IsSet($response['error']))
+ {
+ $this->authorization_error = 'it was not possible to retrieve the access token: it was returned the error: '.$response['error'];
+ return true;
+ }
+ return($this->SetError('OAuth server did not return the access token'));
+ }
+ $access_token = array(
+ 'value'=>($this->access_token = $response['access_token']),
+ 'authorized'=>true,
+ );
+ if($this->store_access_token_response)
+ $access_token['response'] = $this->access_token_response = $response;
+ if($this->debug)
+ $this->OutputDebug('Access token: '.$this->access_token);
+ if(IsSet($response['expires_in'])
+ && $response['expires_in'] == 0)
+ {
+ if($this->debug)
+ $this->OutputDebug('Ignoring access token expiry set to 0');
+ $this->access_token_expiry = '';
+ }
+ elseif(IsSet($response['expires'])
+ || IsSet($response['expires_in']))
+ {
+ $expires = (IsSet($response['expires']) ? $response['expires'] : $response['expires_in']);
+ if(strval($expires) !== strval(intval($expires))
+ || $expires <= 0)
+ return($this->SetError('OAuth server did not return a supported type of access token expiry time'));
+ $this->access_token_expiry = gmstrftime('%Y-%m-%d %H:%M:%S', time() + $expires);
+ if($this->debug)
+ $this->OutputDebug('Access token expiry: '.$this->access_token_expiry.' UTC');
+ $access_token['expiry'] = $this->access_token_expiry;
+ }
+ else
+ $this->access_token_expiry = '';
+ if(IsSet($response['token_type']))
+ {
+ $this->access_token_type = $response['token_type'];
+ if(strlen($this->access_token_type)
+ && $this->debug)
+ $this->OutputDebug('Access token type: '.$this->access_token_type);
+ $access_token['type'] = $this->access_token_type;
+ }
+ else
+ {
+ $this->access_token_type = $this->default_access_token_type;
+ if(strlen($this->access_token_type)
+ && $this->debug)
+ $this->OutputDebug('Assumed the default for OAuth access token type which is '.$this->access_token_type);
+ }
+ if(IsSet($response['refresh_token']))
+ {
+ $this->refresh_token = $response['refresh_token'];
+ if($this->debug)
+ $this->OutputDebug('New refresh token: '.$this->refresh_token);
+ $access_token['refresh'] = $this->refresh_token;
+ }
+ elseif(strlen($this->refresh_token))
+ {
+ if($this->debug)
+ $this->OutputDebug('Reusing previous refresh token: '.$this->refresh_token);
+ $access_token['refresh'] = $this->refresh_token;
+ }
+ if(!$this->StoreAccessToken($access_token))
+ return false;
+ return true;
+ }
+
+ Function RetrieveToken(&$valid)
+ {
+ $valid = false;
+ if(!$this->GetAccessToken($access_token))
+ return false;
+ if(IsSet($access_token['value']))
+ {
+ $this->access_token_expiry = '';
+ $expired = (IsSet($access_token['expiry']) && strcmp($this->access_token_expiry = $access_token['expiry'], gmstrftime('%Y-%m-%d %H:%M:%S')) < 0);
+ if($expired)
+ {
+ if($this->debug)
+ $this->OutputDebug('The OAuth access token expired in '.$this->access_token_expiry);
+ }
+ $this->access_token = $access_token['value'];
+ if(!$expired
+ && $this->debug)
+ $this->OutputDebug('The OAuth access token '.$this->access_token.' is valid');
+ if(IsSet($access_token['type']))
+ {
+ $this->access_token_type = $access_token['type'];
+ if(strlen($this->access_token_type)
+ && !$expired
+ && $this->debug)
+ $this->OutputDebug('The OAuth access token is of type '.$this->access_token_type);
+ }
+ else
+ {
+ $this->access_token_type = $this->default_access_token_type;
+ if(strlen($this->access_token_type)
+ && !$expired
+ && $this->debug)
+ $this->OutputDebug('Assumed the default for OAuth access token type which is '.$this->access_token_type);
+ }
+ if(IsSet($access_token['secret']))
+ {
+ $this->access_token_secret = $access_token['secret'];
+ if($this->debug
+ && !$expired)
+ $this->OutputDebug('The OAuth access token secret is '.$this->access_token_secret);
+ }
+ if(IsSet($access_token['refresh']))
+ $this->refresh_token = $access_token['refresh'];
+ else
+ $this->refresh_token = '';
+ $this->access_token_response = (($this->store_access_token_response && IsSet($access_token['response'])) ? $access_token['response'] : null);
+ $valid = true;
+ }
+ return true;
+ }
+/*
+{metadocument}
+ <function>
+ <name>CallAPI</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Send a HTTP request to the Web services API using a
+ previously obtained authorization token via OAuth.</purpose>
+ <usage>This function can be used to call an API after having
+ previously obtained an access token through the OAuth protocol
+ using the <functionlink>Process</functionlink> function, or by
+ directly setting the variables
+ <variablelink>access_token</variablelink>, as well as
+ <variablelink>access_token_secret</variablelink> in case of using
+ OAuth 1.0 or 1.0a services.</usage>
+ <returnvalue>This function returns <booleanvalue>1</booleanvalue> if
+ the call was done successfully.</returnvalue>
+ </documentation>
+ <argument>
+ <name>url</name>
+ <type>STRING</type>
+ <documentation>
+ <purpose>URL of the API where the HTTP request will be sent.</purpose>
+ </documentation>
+ </argument>
+ <argument>
+ <name>method</name>
+ <type>STRING</type>
+ <documentation>
+ <purpose>HTTP method that will be used to send the request. It can
+ be <stringvalue>GET</stringvalue>,
+ <stringvalue>POST</stringvalue>,
+ <stringvalue>DELETE</stringvalue>, <stringvalue>PUT</stringvalue>,
+ etc..</purpose>
+ </documentation>
+ </argument>
+ <argument>
+ <name>parameters</name>
+ <type>HASH</type>
+ <documentation>
+ <purpose>Associative array with the names and values of the API
+ call request parameters.</purpose>
+ </documentation>
+ </argument>
+ <argument>
+ <name>options</name>
+ <type>HASH</type>
+ <documentation>
+ <purpose>Associative array with additional options to configure
+ the request. Currently it supports the following
+ options:<paragraphbreak />
+ <stringvalue>2Legged</stringvalue>: boolean option that
+ determines if the API request should be 2 legged. The default
+ value is <tt><booleanvalue>0</booleanvalue></tt>.<paragraphbreak />
+ <stringvalue>Accept</stringvalue>: content type value of the
+ Accept HTTP header to be sent in the API call HTTP request.
+ Some APIs require that a certain value be sent to specify
+ which version of the API is being called. The default value is
+ <stringvalue>*/*</stringvalue>.<paragraphbreak />
+ <stringvalue>ConvertObjects</stringvalue>: boolean option that
+ determines if objects should be converted into arrays when the
+ response is returned in JSON format. The default value is
+ <booleanvalue>0</booleanvalue>.<paragraphbreak />
+ <stringvalue>DecodeXMLResponse</stringvalue>: name of the method
+ to decode XML responses. Currently only
+ <stringvalue>simplexml</stringvalue> is supported. It makes a
+ XML response be parsed and returned as a SimpleXMLElement
+ object.<paragraphbreak />
+ <stringvalue>FailOnAccessError</stringvalue>: boolean option
+ that determines if this functions should fail when the server
+ response status is not between 200 and 299. The default value
+ is <booleanvalue>0</booleanvalue>.<paragraphbreak />
+ <stringvalue>Files</stringvalue>: associative array with
+ details of the parameters that must be passed as file uploads.
+ The array indexes must have the same name of the parameters
+ to be sent as files. The respective array entry values must
+ also be associative arrays with the parameters for each file.
+ Currently it supports the following parameters:<paragraphbreak />
+ - <tt>Type</tt> - defines how the parameter value should be
+ treated. It can be <tt>'FileName'</tt> if the parameter value is
+ is the name of a local file to be uploaded. It may also be
+ <tt>'Data'</tt> if the parameter value is the actual data of
+ the file to be uploaded.<paragraphbreak />
+ - Default: <tt>'FileName'</tt><paragraphbreak />
+ - <tt>ContentType</tt> - MIME value of the content type of the
+ file. It can be <tt>'automatic/name'</tt> if the content type
+ should be determine from the file name extension.<paragraphbreak />
+ - Default: <tt>'automatic/name'</tt><paragraphbreak />
+ <stringvalue>PostValuesInURI</stringvalue>: boolean option to
+ determine that a POST request should pass the request values
+ in the URI. The default value is
+ <booleanvalue>0</booleanvalue>.<paragraphbreak />
+ <stringvalue>FollowRedirection</stringvalue>: limit number of
+ times that HTTP response redirects will be followed. If it is
+ set to <integervalue>0</integervalue>, redirection responses
+ fail in error. The default value is
+ <integervalue>0</integervalue>.<paragraphbreak />
+ <stringvalue>RequestBody</stringvalue>: request body data of a
+ custom type. The <stringvalue>RequestContentType</stringvalue>
+ option must be specified, so the
+ <stringvalue>RequestBody</stringvalue> option is considered.<paragraphbreak />
+ <stringvalue>RequestContentType</stringvalue>: content type that
+ should be used to send the request values. It can be either
+ <stringvalue>application/x-www-form-urlencoded</stringvalue>
+ for sending values like from Web forms, or
+ <stringvalue>application/json</stringvalue> for sending the
+ values encoded in JSON format. Other types are accepted if the
+ <stringvalue>RequestBody</stringvalue> option is specified.
+ The default value is
+ <stringvalue>application/x-www-form-urlencoded</stringvalue>.<paragraphbreak />
+ <stringvalue>RequestBody</stringvalue>: request body data of a
+ custom type. The <stringvalue>RequestContentType</stringvalue>
+ option must be specified, so the
+ <stringvalue>RequestBody</stringvalue> option is considered.<paragraphbreak />
+ <stringvalue>Resource</stringvalue>: string with a label that
+ will be used in the error messages and debug log entries to
+ identify what operation the request is performing. The default
+ value is <stringvalue>API call</stringvalue>.<paragraphbreak />
+ <stringvalue>ResponseContentType</stringvalue>: content type
+ that should be considered when decoding the API request
+ response. This overrides the <tt>Content-Type</tt> header
+ returned by the server. If the content type is
+ <stringvalue>application/x-www-form-urlencoded</stringvalue>
+ the function will parse the data returning an array of
+ key-value pairs. If the content type is
+ <stringvalue>application/json</stringvalue> the response will
+ be decode as a JSON-encoded data type. Other content type
+ values will make the function return the original response
+ value as it was returned from the server. The default value
+ for this option is to use what the server returned in the
+ <tt>Content-Type</tt> header.</purpose>
+ </documentation>
+ </argument>
+ <argument>
+ <name>response</name>
+ <type>STRING</type>
+ <out />
+ <documentation>
+ <purpose>Return the value of the API response. If the value is
+ JSON encoded, this function will decode it and return the value
+ converted to respective types. If the value is form encoded,
+ this function will decode the response and return it as an
+ array. Otherwise, the class will return the value as a
+ string.</purpose>
+ </documentation>
+ </argument>
+ <do>
+{/metadocument}
+*/
+ Function CallAPI($url, $method, $parameters, $options, &$response)
+ {
+ if(!IsSet($options['Resource']))
+ $options['Resource'] = 'API call';
+ if(!IsSet($options['ConvertObjects']))
+ $options['ConvertObjects'] = false;
+ if(strlen($this->access_token) === 0)
+ {
+ if(!$this->RetrieveToken($valid))
+ return false;
+ if(!$valid)
+ return $this->SetError('the access token is not set to a valid value');
+ }
+ switch(intval($this->oauth_version))
+ {
+ case 1:
+ $oauth = array(
+ (strlen($this->access_token_parameter) ? $this->access_token_parameter : 'oauth_token')=>((IsSet($options['2Legged']) && $options['2Legged']) ? '' : $this->access_token)
+ );
+ break;
+
+ case 2:
+ if(strlen($this->access_token_expiry)
+ && strcmp($this->access_token_expiry, gmstrftime('%Y-%m-%d %H:%M:%S')) <= 0)
+ {
+ if(strlen($this->refresh_token) === 0)
+ return($this->SetError('the access token expired and no refresh token is available'));
+ if($this->debug)
+ $this->OutputDebug('Refreshing the OAuth access token');
+ if(!$this->ProcessToken(null, true))
+ return false;
+ if(IsSet($options['FailOnAccessError'])
+ && $options['FailOnAccessError']
+ && strlen($this->authorization_error))
+ {
+ $this->error = $this->authorization_error;
+ return false;
+ }
+ }
+ $oauth = null;
+ if(strcasecmp($this->access_token_type, 'Bearer'))
+ $url .= (strcspn($url, '?') < strlen($url) ? '&' : '?').(strlen($this->access_token_parameter) ? $this->access_token_parameter : 'access_token').'='.UrlEncode($this->access_token);
+ break;
+
+ default:
+ return($this->SetError($this->oauth_version.' is not a supported version of the OAuth protocol'));
+ }
+ return($this->SendAPIRequest($url, $method, $parameters, $oauth, $options, $response));
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>Initialize</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Initialize the class variables and internal state. It must
+ be called before calling other class functions.</purpose>
+ <usage>Set the <variablelink>server</variablelink> variable before
+ calling this function to let it initialize the class variables to
+ work with the specified server type. Alternatively, you can set
+ other class variables manually to make it work with servers that
+ are not yet built-in supported.</usage>
+ <returnvalue>This function returns <booleanvalue>1</booleanvalue> if
+ it was able to successfully initialize the class for the specified
+ server type.</returnvalue>
+ </documentation>
+ <do>
+{/metadocument}
+*/
+ Function Initialize()
+ {
+ if(strlen($this->server) === 0)
+ return true;
+ $this->oauth_version =
+ $this->dialog_url =
+ $this->access_token_url =
+ $this->request_token_url =
+ $this->append_state_to_redirect_uri = '';
+ $this->authorization_header = true;
+ $this->url_parameters = false;
+ $this->token_request_method = 'GET';
+ $this->signature_method = 'HMAC-SHA1';
+ $this->access_token_authentication = '';
+ $this->access_token_parameter = '';
+ $this->default_access_token_type = '';
+ $this->store_access_token_response = false;
+ switch($this->server)
+ {
+ case 'Facebook':
+ $this->oauth_version = '2.0';
+ $this->dialog_url = 'https://www.facebook.com/dialog/oauth?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}';
+ $this->access_token_url = 'https://graph.facebook.com/oauth/access_token';
+ break;
+
+ case 'github':
+ $this->oauth_version = '2.0';
+ $this->dialog_url = 'https://github.com/login/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}';
+ $this->access_token_url = 'https://github.com/login/oauth/access_token';
+ break;
+
+ case 'Google':
+ $this->oauth_version = '2.0';
+ $this->dialog_url = 'https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}';
+ $this->offline_dialog_url = 'https://accounts.google.com/o/oauth2/auth?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&access_type=offline&approval_prompt=force';
+ $this->access_token_url = 'https://accounts.google.com/o/oauth2/token';
+ break;
+
+ case 'LinkedIn':
+ $this->oauth_version = '1.0a';
+ $this->request_token_url = 'https://api.linkedin.com/uas/oauth/requestToken?scope={SCOPE}';
+ $this->dialog_url = 'https://api.linkedin.com/uas/oauth/authenticate';
+ $this->access_token_url = 'https://api.linkedin.com/uas/oauth/accessToken';
+ $this->url_parameters = true;
+ break;
+
+ case 'Microsoft':
+ $this->oauth_version = '2.0';
+ $this->dialog_url = 'https://login.live.com/oauth20_authorize.srf?client_id={CLIENT_ID}&scope={SCOPE}&response_type=code&redirect_uri={REDIRECT_URI}&state={STATE}';
+ $this->access_token_url = 'https://login.live.com/oauth20_token.srf';
+ break;
+
+ case 'Twitter':
+ $this->oauth_version = '1.0a';
+ $this->request_token_url = 'https://api.twitter.com/oauth/request_token';
+ $this->dialog_url = 'https://api.twitter.com/oauth/authenticate';
+ $this->access_token_url = 'https://api.twitter.com/oauth/access_token';
+ $this->url_parameters = false;
+ break;
+
+ case 'Yahoo':
+ $this->oauth_version = '1.0a';
+ $this->request_token_url = 'https://api.login.yahoo.com/oauth/v2/get_request_token';
+ $this->dialog_url = 'https://api.login.yahoo.com/oauth/v2/request_auth';
+ $this->access_token_url = 'https://api.login.yahoo.com/oauth/v2/get_token';
+ $this->authorization_header = false;
+ break;
+
+ default:
+ if(!($json = @file_get_contents($this->configuration_file)))
+ {
+ if(!file_exists($this->configuration_file))
+ return $this->SetError('the OAuth server configuration file '.$this->configuration_file.' does not exist');
+ return $this->SetPHPError('could not read the OAuth server configuration file '.$this->configuration_file, $php_errormsg);
+ }
+ $oauth_server = json_decode($json);
+ if(!IsSet($oauth_server))
+ return $this->SetPHPError('It was not possible to decode the OAuth server configuration file '.$this->configuration_file.' eventually due to incorrect format', $php_errormsg);
+ if(GetType($oauth_server) !== 'object')
+ return $this->SetError('It was not possible to decode the OAuth server configuration file '.$this->configuration_file.' because it does not correctly define a JSON object');
+ if(!IsSet($oauth_server->servers)
+ || GetType($oauth_server->servers) !== 'object')
+ return $this->SetError('It was not possible to decode the OAuth server configuration file '.$this->configuration_file.' because it does not correctly define a JSON object for servers');
+ if(!IsSet($oauth_server->servers->{$this->server}))
+ return($this->SetError($this->server.' is not yet a supported type of OAuth server. Please send a request in this class support forum (preferred) http://www.phpclasses.org/oauth-api , or if it is a security or private matter, contact the author Manuel Lemos mlemos@acm.org to request adding built-in support to this type of OAuth server.'));
+ $properties = $oauth_server->servers->{$this->server};
+ if(GetType($properties) !== 'object')
+ return $this->SetError('The OAuth server configuration file '.$this->configuration_file.' for the "'.$this->server.'" server does not correctly define a JSON object');
+ $types = array(
+ 'oauth_version'=>'string',
+ 'request_token_url'=>'string',
+ 'dialog_url'=>'string',
+ 'offline_dialog_url'=>'string',
+ 'access_token_url'=>'string',
+ 'append_state_to_redirect_uri'=> 'string',
+ 'authorization_header'=>'boolean',
+ 'url_parameters' => 'boolean',
+ 'token_request_method'=>'string',
+ 'signature_method'=>'string',
+ 'access_token_authentication'=>'string',
+ 'access_token_parameter'=>'string',
+ 'default_access_token_type'=>'string',
+ 'store_access_token_response'=>'boolean'
+ );
+ $required = array(
+ 'oauth_version'=>array(),
+ 'request_token_url'=>array('1.0', '1.0a'),
+ 'dialog_url'=>array(),
+ 'access_token_url'=>array(),
+ );
+ foreach($properties as $property => $value)
+ {
+ if(!IsSet($types[$property]))
+ return $this->SetError($property.' is not a supported property for the "'.$this->server.'" server in the OAuth server configuration file '.$this->configuration_file);
+ $type = GetType($value);
+ $expected = $types[$property];
+ if($type !== $expected)
+ return $this->SetError(' the property "'.$property.'" for the "'.$this->server.'" server is not of type "'.$expected.'", it is of type "'.$type.'", in the OAuth server configuration file '.$this->configuration_file);
+ $this->{$property} = $value;
+ UnSet($required[$property]);
+ }
+ foreach($required as $property => $value)
+ {
+ if(count($value)
+ && in_array($this->oauth_version, $value))
+ return $this->SetError('the property "'.$property.'" is not defined for the "'.$this->server.'" server in the OAuth server configuration file '.$this->configuration_file);
+ }
+ break;
+ }
+ return(true);
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>Process</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Process the OAuth protocol interaction with the OAuth
+ server.</purpose>
+ <usage>Call this function when you need to retrieve the OAuth access
+ token. Check the <variablelink>access_token</variablelink> to
+ determine if the access token was obtained successfully.</usage>
+ <returnvalue>This function returns <booleanvalue>1</booleanvalue> if
+ the OAuth protocol was processed without errors.</returnvalue>
+ </documentation>
+ <do>
+{/metadocument}
+*/
+ Function Process()
+ {
+ switch(intval($this->oauth_version))
+ {
+ case 1:
+ $one_a = ($this->oauth_version === '1.0a');
+ if($this->debug)
+ $this->OutputDebug('Checking the OAuth token authorization state');
+ if(!$this->GetAccessToken($access_token))
+ return false;
+ if(IsSet($access_token['authorized'])
+ && IsSet($access_token['value']))
+ {
+ $expired = (IsSet($access_token['expiry']) && strcmp($access_token['expiry'], gmstrftime('%Y-%m-%d %H:%M:%S')) <= 0);
+ if(!$access_token['authorized']
+ || $expired)
+ {
+ if($this->debug)
+ {
+ if($expired)
+ $this->OutputDebug('The OAuth token expired on '.$access_token['expiry'].'UTC');
+ else
+ $this->OutputDebug('The OAuth token is not yet authorized');
+ $this->OutputDebug('Checking the OAuth token and verifier');
+ }
+ if(!$this->GetRequestToken($token, $verifier))
+ return false;
+ if(!IsSet($token)
+ || ($one_a
+ && !IsSet($verifier)))
+ {
+ if(!$this->GetRequestDenied($denied))
+ return false;
+ if(IsSet($denied)
+ && $denied === $access_token['value'])
+ {
+ if($this->debug)
+ $this->OutputDebug('The authorization request was denied');
+ $this->authorization_error = 'the request was denied';
+ return true;
+ }
+ else
+ {
+ if($this->debug)
+ $this->OutputDebug('Reset the OAuth token state because token and verifier are not both set');
+ $access_token = array();
+ }
+ }
+ elseif($token !== $access_token['value'])
+ {
+ if($this->debug)
+ $this->OutputDebug('Reset the OAuth token state because token does not match what as previously retrieved');
+ $access_token = array();
+ }
+ else
+ {
+ if(!$this->GetAccessTokenURL($url))
+ return false;
+ $oauth = array(
+ 'oauth_token'=>$token,
+ );
+ if($one_a)
+ $oauth['oauth_verifier'] = $verifier;
+ $this->access_token_secret = $access_token['secret'];
+ $options = array('Resource'=>'OAuth access token');
+ $method = strtoupper($this->token_request_method);
+ switch($method)
+ {
+ case 'GET':
+ break;
+ case 'POST':
+ $options['PostValuesInURI'] = true;
+ break;
+ default:
+ $this->error = $method.' is not a supported method to request tokens';
+ break;
+ }
+ if(!$this->SendAPIRequest($url, $method, array(), $oauth, $options, $response))
+ return false;
+ if(strlen($this->access_token_error))
+ {
+ $this->authorization_error = $this->access_token_error;
+ return true;
+ }
+ if(!IsSet($response['oauth_token'])
+ || !IsSet($response['oauth_token_secret']))
+ {
+ $this->authorization_error= 'it was not returned the access token and secret';
+ return true;
+ }
+ $access_token = array(
+ 'value'=>$response['oauth_token'],
+ 'secret'=>$response['oauth_token_secret'],
+ 'authorized'=>true
+ );
+ if(IsSet($response['oauth_expires_in'])
+ && $response['oauth_expires_in'] == 0)
+ {
+ if($this->debug)
+ $this->OutputDebug('Ignoring access token expiry set to 0');
+ $this->access_token_expiry = '';
+ }
+ elseif(IsSet($response['oauth_expires_in']))
+ {
+ $expires = $response['oauth_expires_in'];
+ if(strval($expires) !== strval(intval($expires))
+ || $expires <= 0)
+ return($this->SetError('OAuth server did not return a supported type of access token expiry time'));
+ $this->access_token_expiry = gmstrftime('%Y-%m-%d %H:%M:%S', time() + $expires);
+ if($this->debug)
+ $this->OutputDebug('Access token expiry: '.$this->access_token_expiry.' UTC');
+ $access_token['expiry'] = $this->access_token_expiry;
+ }
+ else
+ $this->access_token_expiry = '';
+
+ if(!$this->StoreAccessToken($access_token))
+ return false;
+ if($this->debug)
+ $this->OutputDebug('The OAuth token was authorized');
+ }
+ }
+ elseif($this->debug)
+ $this->OutputDebug('The OAuth token was already authorized');
+ if(IsSet($access_token['authorized'])
+ && $access_token['authorized'])
+ {
+ $this->access_token = $access_token['value'];
+ $this->access_token_secret = $access_token['secret'];
+ return true;
+ }
+ }
+ else
+ {
+ if($this->debug)
+ $this->OutputDebug('The OAuth access token is not set');
+ $access_token = array();
+ }
+ if(!IsSet($access_token['authorized']))
+ {
+ if($this->debug)
+ $this->OutputDebug('Requesting the unauthorized OAuth token');
+ if(!$this->GetRequestTokenURL($url))
+ return false;
+ $url = str_replace('{SCOPE}', UrlEncode($this->scope), $url);
+ if(!$this->GetRedirectURI($redirect_uri))
+ return false;
+ $oauth = array(
+ 'oauth_callback'=>$redirect_uri,
+ );
+ $options = array(
+ 'Resource'=>'OAuth request token',
+ 'FailOnAccessError'=>true
+ );
+ $method = strtoupper($this->token_request_method);
+ switch($method)
+ {
+ case 'GET':
+ break;
+ case 'POST':
+ $options['PostValuesInURI'] = true;
+ break;
+ default:
+ $this->error = $method.' is not a supported method to request tokens';
+ break;
+ }
+ if(!$this->SendAPIRequest($url, $method, array(), $oauth, $options, $response))
+ return false;
+ if(strlen($this->access_token_error))
+ {
+ $this->authorization_error = $this->access_token_error;
+ return true;
+ }
+ if(!IsSet($response['oauth_token'])
+ || !IsSet($response['oauth_token_secret']))
+ {
+ $this->authorization_error = 'it was not returned the requested token';
+ return true;
+ }
+ $access_token = array(
+ 'value'=>$response['oauth_token'],
+ 'secret'=>$response['oauth_token_secret'],
+ 'authorized'=>false
+ );
+ if(IsSet($response['login_url']))
+ $access_token['login_url'] = $response['login_url'];
+ if(!$this->StoreAccessToken($access_token))
+ return false;
+ }
+ if(!$this->GetDialogURL($url))
+ return false;
+ if($url === 'automatic')
+ {
+ if(!IsSet($access_token['login_url']))
+ return($this->SetError('The request token response did not automatically the login dialog URL as expected'));
+ if($this->debug)
+ $this->OutputDebug('Dialog URL obtained automatically from the request token response: '.$url);
+ $url = $access_token['login_url'];
+ }
+ else
+ $url .= (strpos($url, '?') === false ? '?' : '&').'oauth_token='.$access_token['value'];
+ if(!$one_a)
+ {
+ if(!$this->GetRedirectURI($redirect_uri))
+ return false;
+ $url .= '&oauth_callback='.UrlEncode($redirect_uri);
+ }
+ if($this->debug)
+ $this->OutputDebug('Redirecting to OAuth authorize page '.$url);
+ $this->Redirect($url);
+ $this->exit = true;
+ return true;
+
+ case 2:
+ if($this->debug)
+ {
+ if(!$this->GetAccessTokenURL($access_token_url))
+ return false;
+ $this->OutputDebug('Checking if OAuth access token was already retrieved from '.$access_token_url);
+ }
+ if(!$this->RetrieveToken($valid))
+ return false;
+ if($valid)
+ return true;
+ if($this->debug)
+ $this->OutputDebug('Checking the authentication state in URI '.$_SERVER['REQUEST_URI']);
+ if(!$this->GetStoredState($stored_state))
+ return false;
+ if(strlen($stored_state) == 0)
+ return($this->SetError('it was not set the OAuth state'));
+ if(!$this->GetRequestState($state))
+ return false;
+ if($state === $stored_state)
+ {
+ if($this->debug)
+ $this->OutputDebug('Checking the authentication code');
+ if(!$this->GetRequestCode($code))
+ return false;
+ if(strlen($code) == 0)
+ {
+ if(!$this->GetRequestError($this->authorization_error))
+ return false;
+ if(IsSet($this->authorization_error))
+ {
+ if($this->debug)
+ $this->OutputDebug('Authorization failed with error code '.$this->authorization_error);
+ switch($this->authorization_error)
+ {
+ case 'invalid_request':
+ case 'unauthorized_client':
+ case 'access_denied':
+ case 'unsupported_response_type':
+ case 'invalid_scope':
+ case 'server_error':
+ case 'temporarily_unavailable':
+ case 'user_denied':
+ return true;
+ default:
+ return($this->SetError('it was returned an unknown OAuth error code'));
+ }
+ }
+ return($this->SetError('it was not returned the OAuth dialog code'));
+ }
+ if(!$this->ProcessToken($code, false))
+ return false;
+ }
+ else
+ {
+ if(!$this->GetRedirectURI($redirect_uri))
+ return false;
+ if(strlen($this->append_state_to_redirect_uri))
+ $redirect_uri .= (strpos($redirect_uri, '?') === false ? '?' : '&').$this->append_state_to_redirect_uri.'='.$stored_state;
+ if(!$this->GetDialogURL($url, $redirect_uri, $stored_state))
+ return false;
+ if(strlen($url) == 0)
+ return($this->SetError('it was not set the OAuth dialog URL'));
+ if($this->debug)
+ $this->OutputDebug('Redirecting to OAuth Dialog '.$url);
+ $this->Redirect($url);
+ $this->exit = true;
+ }
+ break;
+
+ default:
+ return($this->SetError($this->oauth_version.' is not a supported version of the OAuth protocol'));
+ }
+ return(true);
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>Finalize</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Cleanup any resources that may have been used during the
+ OAuth protocol processing or execution of API calls.</purpose>
+ <usage>Always call this function as the last step after calling the
+ functions <functionlink>Process</functionlink> or
+ <functionlink>CallAPI</functionlink>.</usage>
+ <returnvalue>This function returns <booleanvalue>1</booleanvalue> if
+ the function cleaned up any resources successfully.</returnvalue>
+ </documentation>
+ <argument>
+ <name>success</name>
+ <type>BOOLEAN</type>
+ <documentation>
+ <purpose>Pass the last success state returned by the class or any
+ external code processing the class function results.</purpose>
+ </documentation>
+ </argument>
+ <do>
+{/metadocument}
+*/
+ Function Finalize($success)
+ {
+ return($success);
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+/*
+{metadocument}
+ <function>
+ <name>Output</name>
+ <type>VOID</type>
+ <documentation>
+ <purpose>Display the results of the OAuth protocol processing.</purpose>
+ <usage>Only call this function if you are debugging the OAuth
+ authorization process and you need to view what was its
+ results.</usage>
+ </documentation>
+ <do>
+{/metadocument}
+*/
+ Function Output()
+ {
+ if(strlen($this->authorization_error)
+ || strlen($this->access_token_error)
+ || strlen($this->access_token))
+ {
+?>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+<title>OAuth client result</title>
+</head>
+<body>
+<h1>OAuth client result</h1>
+<?php
+ if(strlen($this->authorization_error))
+ {
+?>
+<p>It was not possible to authorize the application.<?php
+ if($this->debug)
+ {
+?>
+<br>Authorization error: <?php echo HtmlSpecialChars($this->authorization_error);
+ }
+?></p>
+<?php
+ }
+ elseif(strlen($this->access_token_error))
+ {
+?>
+<p>It was not possible to use the application access token.
+<?php
+ if($this->debug)
+ {
+?>
+<br>Error: <?php echo HtmlSpecialChars($this->access_token_error);
+ }
+?></p>
+<?php
+ }
+ elseif(strlen($this->access_token))
+ {
+?>
+<p>The application authorization was obtained successfully.
+<?php
+ if($this->debug)
+ {
+?>
+<br>Access token: <?php echo HtmlSpecialChars($this->access_token);
+ if(IsSet($this->access_token_secret))
+ {
+?>
+<br>Access token secret: <?php echo HtmlSpecialChars($this->access_token_secret);
+ }
+ }
+?></p>
+<?php
+ if(strlen($this->access_token_expiry))
+ {
+?>
+<p>Access token expiry: <?php echo $this->access_token_expiry; ?> UTC</p>
+<?php
+ }
+ }
+?>
+</body>
+</html>
+<?php
+ }
+ }
+/*
+{metadocument}
+ </do>
+ </function>
+{/metadocument}
+*/
+
+};
+
+/*
+
+{metadocument}
+</class>
+{/metadocument}
+
+*/
+
+?>
\ No newline at end of file
--- /dev/null
+{
+ "version": "$Id: oauth_configuration.json,v 1.3 2014/04/11 10:25:46 mlemos Exp $",
+ "comments": [
+ "The servers entry should be an object with a list of object",
+ "entries, one for each server type. The server object entry name is",
+ "the name of the server type. Each server entry is an object with",
+ "some mandatory properties: oauth_version, dialog_url,",
+ "access_token_url and request_token_url (just for Oauth 1.0 and",
+ "1.0a). Check the OAuth client class for the complete list of server",
+ "properties."
+ ],
+ "servers":
+ {
+ "37Signals":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://launchpad.37signals.com/authorization/new?type=web_server&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&state={STATE}",
+ "access_token_url": "https://launchpad.37signals.com/authorization/token?type=web_server"
+ },
+
+ "Amazon":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://www.amazon.com/ap/oa?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&response_type=code&state={STATE}",
+ "access_token_url": "https://api.amazon.com/auth/o2/token"
+ },
+
+ "Bitbucket":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://bitbucket.org/!api/1.0/oauth/request_token",
+ "dialog_url": "https://bitbucket.org/!api/1.0/oauth/authenticate",
+ "access_token_url": "https://bitbucket.org/!api/1.0/oauth/access_token",
+ "url_parameters": false
+ },
+
+ "Box":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://www.box.com/api/oauth2/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&state={STATE}",
+ "offline_dialog_url": "https://www.box.com/api/oauth2/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&state={STATE}&access_type=offline&approval_prompt=force",
+ "access_token_url": "https://www.box.com/api/oauth2/token"
+ },
+
+ "Buffer":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://bufferapp.com/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={STATE}",
+ "access_token_url": "https://api.bufferapp.com/1/oauth2/token.json"
+ },
+
+ "Discogs":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://api.discogs.com/oauth/request_token",
+ "dialog_url": "http://www.discogs.com/oauth/authorize",
+ "access_token_url": "http://api.discogs.com/oauth/access_token"
+ },
+
+ "Disqus":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://disqus.com/api/oauth/2.0/authorize/?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "access_token_url": "https://disqus.com/api/oauth/2.0/access_token/"
+ },
+
+ "Dropbox":
+ {
+ "oauth_version": "1.0",
+ "request_token_url": "https://api.dropbox.com/1/oauth/request_token",
+ "dialog_url": "https://www.dropbox.com/1/oauth/authorize",
+ "access_token_url": "https://api.dropbox.com/1/oauth/access_token",
+ "authorization_header": false
+ },
+
+ "Dropbox2":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://www.dropbox.com/1/oauth2/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "access_token_url": "https://www.dropbox.com/1/oauth2/token"
+ },
+
+ "Etsy":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://openapi.etsy.com/v2/oauth/request_token?scope={SCOPE}",
+ "dialog_url": "automatic",
+ "access_token_url": "https://openapi.etsy.com/v2/oauth/access_token"
+ },
+
+ "Eventful":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://eventful.com/oauth/request_token",
+ "dialog_url": "http://eventful.com/oauth/authorize",
+ "access_token_url": "http://eventful.com/oauth/access_token",
+ "authorization_header": false,
+ "url_parameters": true,
+ "token_request_method": "POST"
+ },
+
+ "Evernote":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://sandbox.evernote.com/oauth",
+ "dialog_url": "https://sandbox.evernote.com/OAuth.action",
+ "access_token_url": "https://sandbox.evernote.com/oauth",
+ "url_parameters": true,
+ "authorization_header": false
+ },
+
+ "Fitbit":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://api.fitbit.com/oauth/request_token",
+ "dialog_url": "http://api.fitbit.com/oauth/authorize",
+ "access_token_url": "http://api.fitbit.com/oauth/access_token"
+ },
+
+ "Flickr":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://www.flickr.com/services/oauth/request_token",
+ "dialog_url": "http://www.flickr.com/services/oauth/authorize?perms={SCOPE}",
+ "access_token_url": "http://www.flickr.com/services/oauth/access_token",
+ "authorization_header": false
+ },
+
+ "Foursquare":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://foursquare.com/oauth2/authorize?client_id={CLIENT_ID}&scope={SCOPE}&response_type=code&redirect_uri={REDIRECT_URI}&state={STATE}",
+ "access_token_url": "https://foursquare.com/oauth2/access_token",
+ "access_token_parameter": "oauth_token"
+ },
+
+ "Google1":
+ {
+ "oauth_version": "1.0a",
+ "dialog_url": "https://www.google.com/accounts/OAuthAuthorizeToken",
+ "access_token_url": "https://www.google.com/accounts/OAuthGetAccessToken",
+ "request_token_url": "https://www.google.com/accounts/OAuthGetRequestToken?scope={SCOPE}"
+ },
+
+ "Instagram":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://api.instagram.com/oauth/authorize/?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&response_type=code&state={STATE}",
+ "access_token_url": "https://api.instagram.com/oauth/access_token"
+ },
+
+ "Rdio":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://api.rdio.com/oauth/request_token",
+ "dialog_url": "https://www.rdio.com/oauth/authorize",
+ "access_token_url": "http://api.rdio.com/oauth/access_token"
+ },
+
+ "Reddit":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://ssl.reddit.com/api/v1/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "offline_dialog_url": "https://ssl.reddit.com/api/v1/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}&duration=permanent",
+ "access_token_url": "https://ssl.reddit.com/api/v1/access_token",
+ "access_token_authentication": "basic"
+ },
+
+ "RightSignature":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://rightsignature.com/oauth/request_token",
+ "dialog_url": "https://rightsignature.com/oauth/authorize",
+ "access_token_url": "https://rightsignature.com/oauth/access_token",
+ "authorization_header": false
+ },
+
+ "Salesforce":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://login.salesforce.com/services/oauth2/authorize?response_type=code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "access_token_url": "https://login.salesforce.com/services/oauth2/token",
+ "default_access_token_type": "Bearer",
+ "store_access_token_response": true
+ },
+
+ "Scoop.it":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://www.scoop.it/oauth/request",
+ "dialog_url": "https://www.scoop.it/oauth/authorize",
+ "access_token_url": "https://www.scoop.it/oauth/access",
+ "authorization_header": false
+ },
+
+ "StockTwits":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://api.stocktwits.com/api/2/oauth/authorize?client_id={CLIENT_ID}&response_type=code&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "access_token_url": "https://api.stocktwits.com/api/2/oauth/token"
+ },
+
+ "SurveyMonkey":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://api.surveymonkey.net/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={STATE}&api_key={API_KEY}",
+ "access_token_url": "https://api.surveymonkey.net/oauth/token?api_key={API_KEY}"
+ },
+
+ "Tumblr":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "http://www.tumblr.com/oauth/request_token",
+ "dialog_url": "http://www.tumblr.com/oauth/authorize",
+ "access_token_url": "http://www.tumblr.com/oauth/access_token"
+ },
+
+ "Vimeo":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://api.vimeo.com/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={STATE}",
+ "access_token_url": "https://api.vimeo.com/oauth/access_token"
+ },
+
+ "VK":
+ {
+ "oauth_version": "2.0",
+ "dialog_url": "https://oauth.vk.com/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&state={STATE}",
+ "access_token_url": "https://oauth.vk.com/access_token"
+ },
+
+ "Withings":
+ {
+ "oauth_version": "1.0",
+ "request_token_url": "https://oauth.withings.com/account/request_token",
+ "dialog_url": "https://oauth.withings.com/account/authorize",
+ "access_token_url": "https://oauth.withings.com/account/access_token",
+ "authorization_header": false
+ },
+
+ "Xero":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://api.xero.com/oauth/RequestToken",
+ "dialog_url": "https://api.xero.com/oauth/Authorize",
+ "access_token_url": "https://api.xero.com/oauth/AccessToken"
+ },
+
+ "XING":
+ {
+ "oauth_version": "1.0a",
+ "request_token_url": "https://api.xing.com/v1/request_token",
+ "dialog_url": "https://api.xing.com/v1/authorize",
+ "access_token_url": "https://api.xing.com/v1/access_token",
+ "authorization_header": false
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+=== Tumblr Crosspostr ===\r
+Contributors: meitar\r
+Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted\r
+Tags: tumblr, post, crosspost, publishing, post formats\r
+Requires at least: 3.1\r
+Tested up to: 4.3\r
+Stable tag: 0.8.4\r
+License: GPLv3\r
+License URI: https://www.gnu.org/licenses/gpl-3.0.html\r
+\r
+Tumblr Crosspostr cross-posts your WordPress entries to Tumblr. Changes to your WordPress posts are reflected in your Tumblr posts.\r
+\r
+== Description ==\r
+\r
+Tumblr Crosspostr posts to Tumblr whenever you hit the "Publish" (or "Save Draft") button. It can import your reblogs on Tumblr as native WordPress posts. It even downloads the images in your Photo posts and saves them in the WordPress Media Library.\r
+\r
+* Transform your WordPress website into a back-end for Tumblr.\r
+* Create original posts using WordPress, but publish them to Tumblr.\r
+* Import your Tumblr reblogs automatically.\r
+* [Always have a portable copy (a running backup) of your entire Tumblr blog](http://maymay.net/blog/2014/02/17/keep-a-running-backup-of-your-tumblr-reblogs-with-tumblr-crosspostr/).\r
+\r
+*Donations for this plugin make up a chunk of my income. If you continue to enjoy this plugin, please consider [making a donation](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted). :) Thank you for your support!*\r
+\r
+Tumblr Crosspostr uses Tumblr's simple API to keep posts in sync; when you edit your WordPress post, it updates your Tumblr post. Private WordPress posts become private Tumblr posts, deleting a post from WordPress that you've previously cross-posted to Tumblr deletes it from Tumblr, too, and so on. Scheduling a WordPress post to be published any time in the future will add it to the Tumblr blog's Queue. (However, *the publishing schedule of your Tumblr queue will take precedence*, so be careful!)\r
+\r
+Tumblr Crosspostr is very lightweight. It just requires you to connect to your Tumblr account from the plugin options screen. After that, you're ready to cross-post! See the [Screenshots](https://wordpress.org/plugins/tumblr-crosspostr/screenshots/) for a walk through of this process.\r
+\r
+Tumblr Crosspostr uses [post formats](http://codex.wordpress.org/Post_Formats) to set the appropriate Tumblr post type. The first image, video embed, etcetera that Tumblr Crosspostr detects will be used as the primary media for the Tumblr post type. To take full advantage of Tumblr Crosspostr, I suggest you choose a WordPress theme that supports all the post formats that your Tumblr theme supports, but Tumblr Crosspostr will still work even if your theme does not natively support this feature. :)\r
+\r
+The WordPress post format to Tumblr post type mapping looks like this:\r
+\r
+* WordPress's `Standard`, `Aside`, and `Status` post formats become Tumblr's `Text` post type\r
+* WordPress's `Image` post format becomes Tumblr's `Photo` post type\r
+* WordPress's `Video` post format becomes Tumblr's `Video` post type\r
+* WordPress's `Audio` post format becomes Tumblr's `Audio` post type\r
+* WordPress's `Quote` post format becomes Tumblr's `Quote` post type\r
+* WordPress's `Link` post format becomes Tumblr's `Link` post type\r
+* WordPress's `Chat` post format becomes Tumblr's `Chat` post type\r
+* WordPress's `Gallery` post format becomes Tumblr's `Photoset` post type (sadly this is not yet implemented, but maybe one day soon!!)\r
+\r
+Other options enable tweaking additional metadata from your WordPress entry (notably tags and "Content source" attributions) to Tumblr, sending all your post archives to Tumblr in one click, and more.\r
+\r
+> Servers no longer serve, they possess. We should call them possessors.\r
+\r
+--[Ward Cunningham](https://twitter.com/WardCunningham/status/289875660246220800)\r
+\r
+Learn more about how you can use this plugin to own your own data in conjunction with [the "Bring Your Own Content" self-hosted Web publishing virtual appliance](http://maymay.net/blog/2014/03/13/bring-your-own-content-virtual-self-hosting-web-publishing/).\r
+\r
+\r
+== Installation ==\r
+\r
+1. Download the plugin file.\r
+1. Unzip the file into your 'wp-content/plugins/' directory.\r
+1. Go to your WordPress administration panel and activate the plugin.\r
+1. Go to Tumblr Crosspostr Settings (from the Settings menu) and either create or enter your Tumblr OAuth consumer key and consumer secret. Then click "Save Changes."\r
+1. Once you've entered your consumer key and consumer secret, a "Connect to Tumblr" button will appear. Click that to be redirected to Tumblr's authorization page.\r
+1. Click "Allow" to grant access to your blog from Tumblr Crosspostr.\r
+1. Start posting!!!\r
+\r
+See also the [Screenshots](https://wordpress.org/plugins/tumblr-crosspostr/screenshots/) section for a visual walk through of this process.\r
+\r
+= Installation notes and troubleshooting =\r
+\r
+Tumblr Crosspostr makes use of Manuel Lemos's `oauth_client_class` for some core functions. Most systems have the required packages installed already, but if you notice any errors upon plugin activation, first check to ensure your system's [PHP include path](http://php.net/manual/ini.core.php#ini.include-path) is set correctly. The `lib` directory and its required files look like this:\r
+\r
+ lib\r
+ ├── OAuthWP.php\r
+ ├── OAuthWP_Tumblr.php\r
+ ├── TumblrCrosspostrAPIClient.php\r
+ ├── httpclient\r
+ │ ├── LICENSE.txt\r
+ │ └── http.php\r
+ └── oauth_api\r
+ ├── LICENSE\r
+ ├── oauth_client.php\r
+ └── oauth_configuration.json\r
+\r
+It's also possible that your system administrator will apply updates to one or more of the core system packages this plugin uses without your knowledge. If this happens, and the updated packages contain backward-incompatible changes, the plugin may begin to issue errors. Should this occur, please [file a bug report on the Tumblr Crosspostr project's issue tracker](https://github.com/meitar/tumblr-crosspostr/issues/new).\r
+\r
+== Frequently Asked Questions ==\r
+\r
+= Can I specify a post's tags? =\r
+\r
+Yes. WordPress's tags are also crossposted to Tumblr. If you'd like to keep your WordPress tags separate from your Tumblr tags, be certain you've enabled the "Do not send post tags to Tumblr" setting.\r
+\r
+Additionally, the "Automatically add these tags to all crossposts" setting lets you enter a comma-separated list of tags that will always be applied to your Tumblr crossposts.\r
+\r
+= Does Tumblr Crosspostr properly attribute content sources? =\r
+\r
+Yes. By default, Tumblr Crosspostr will set itself up so that your WordPress blog's posts are attributed as the "Source" for each of your crossposts. Moreover, in each of your posts, you can enter a "Content source" URL in exactly the way Tumblr's own post editor lets you attribute sources, which will be entered as the "Content source" meta field on your Tumblr posts. You can even turn this feature off entirely if you're using Tumblr Crosspostr "secretly," as the back-end to a more elaborate publishing platform might do.\r
+\r
+= Can I send older WordPress posts to Tumblr? =\r
+\r
+Yes. Go edit the desired post, verify the crosspost option is set to `Yes`, and update the post. Tumblr Crosspostr will keep the original post date. Note that sometimes it seems to take Tumblr a few minutes to reflect many new changes, so you may want to use [Tumblr's "mega editor"](http://staff.tumblr.com/post/746164238/mega-editor) to verify that your post really made it over to Tumblr.\r
+\r
+= What if I edit a post that has been cross-posted? =\r
+\r
+If you edit or delete a post, changes will appear on or disappear from Tumblr accordingly.\r
+\r
+= Can I cross-post Private posts from WordPress to Tumblr? =\r
+\r
+Yes. Tumblr Crosspostr respects the WordPress post visibility setting and supports cross-posting private posts to Tumblr. Editing the visibility setting of your WordPress post will update your Tumblr cross-post with the new setting, as well.\r
+\r
+= Can I cross-post custom post types? =\r
+\r
+Yes. By default, Tumblr Crosspostr only crossposts `post` post types, but you can enable or disable other post types from the plugin's settings screen.\r
+\r
+If you're a plugin developer, you can easily make your custom post types work well with Tumblr Crosspostr by implementing the `tumblr_crosspostr_save_post_types`, `tumblr_crosspostr_meta_box_post_types`, and `tumblr_crosspostr_prepared_post` filter hooks. See [Other Notes](https://wordpress.org/plugins/tumblr-crosspostr/other_notes/) for coding details.\r
+\r
+= Is Tumblr Crosspostr available in languages other than English? =\r
+\r
+This plugin has been translated into the following languages:\r
+\r
+* French (`fr_FR`)\r
+ * Thanks, [Julien](http://ijulien.com/)! :D\r
+\r
+With your help it can be translated into even more! To contribute a translation of this plugin into your language, please [sign up as a translator on Tumblr Crosspostr's Transifex project page](https://www.transifex.com/projects/p/tumblr-crosspostr/).\r
+\r
+= What if my theme doesn't support Post Formats? =\r
+\r
+Tumblr Crosspostr will still work even if your theme doesn't support the [Post Formats](http://codex.wordpress.org/Post_Formats) feature. However, consider asking your theme developer to update your theme code so that it supports Post Formats itself for other plugins to use, too.\r
+\r
+If you feel comfortable doing this yourself, then in most cases, this is literally a one-line change. Simply use the [add_theme_support()](http://codex.wordpress.org/Function_Reference/add_theme_support) function in your theme's `functions.php` file:\r
+\r
+ add_theme_support('post-formats', array('link', 'image', 'quote', 'video', 'audio', 'chat''));\r
+\r
+And if you choose to do this yourself, consider getting in touch with your theme's developer to let them know how easy it was! We devs love to hear this kind of stuff. :)\r
+\r
+= Why won't my Tumblr post appear on my WordPress blog immediately? =\r
+\r
+Unfortunately, Tumblr does not provide a programmatic "export" feature, so there is no way to push posts out from Tumblr. This is known as a "[data silo](https://indiewebcamp.com/silo)" and it's always enforced in the interest of corporate control so that humans are turned into dollars. Think Facebook, for example: you can easily put stuff into Facebook, but it's much harder to get that same stuff out. This is the exact opposite of WordPress in every way, both philosophically and technologically. Tumblr, in this case, is like Facebook. It, too, allows you to easily put stuff into it, but it's very hard to take stuff back out.\r
+\r
+Tumblr Crosspostr's Tumblr Sync feature was built to work despite this harsh reality. One of the limitations is that Tumblr Crosspostr Sync can not detect when you have published a new post on Tumblr (because Tumblr never notifies your WordPress blog that this has happened). Instead, it must periodically check your Tumblr blog on its own. It polls your Tumblr blog once every twenty four hours, and then does its best to identify which posts are new. This usually works quite well, but it does mean that it can take up to 24 hours for posts created on Tumblr to show up on your WordPress website. This is another reason why creating posts on WordPress is better than creating posts on Tumblr.\r
+\r
+If you'd like to see a world without arbitrary and unnecessary limitations that only serve corporate overseers like this, consider encouraging your friends to join and support free software platforms like WordPress, [Diaspora](https://wordpress.org/plugins/diasposter/), and other systems that let you own your own data.\r
+\r
+== Screenshots ==\r
+\r
+1. When you first install Tumblr Crosspostr, you'll need to connect it to your Tumblr account before you can start crossposting. This screenshot shows how its options screen first appears after you activate the plugin.\r
+\r
+2. Once you create and enter your API key and click "Save Changes," the options screen prompts you to connect to Tumblr with another button. Press the "Click here to connect to Tumblr" button to begin the OAuth connection process.\r
+\r
+3. After allowing Tumblr Crosspostr access to your Tumblr account, you'll find you're able to access the remainder of the options page. You must choose at least one default Tumblr blog to send your crossposts to, so this option is highlighted if it is not yet set. Set your cross-posting preferences and click "Save Changes." You're now ready to start crossposting!\r
+\r
+4. You can optionally choose not to crosspost individual WordPress posts from the Tumblr Crosspostr custom post editing box. This box also enables you to send a specific post to a Tumblr blog other than the default one you selected in the previous step, send the post's excerpt rather than its main body to Tumblr, and [control the Tumblr auto-tweet](http://www.tumblr.com/docs/twitter) setting (if enabled on your Tumblr blog).\r
+\r
+5. If you already have a lot of content that you want to quickly copy to Tumblr, you can use the "Tumblrize Archives" tool to do exactly that.\r
+\r
+6. Get help where you need it from WordPress's built-in "Help" system.\r
+\r
+== Changelog ==\r
+\r
+= Version 0.8.4 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/warning-invalid-argument-12): Fix "invalid argument" error for some people when they first install the plugin.\r
+* [Bugfix](https://wordpress.org/support/topic/strip-html-in-titles-before-posting-to-tumblr): Strip HTML tags from titles on Tumblr posts.\r
+* Bugfix: Fix "undefined index" error for installations in strict environments when the tweet option is enabled but the tweet text is left blank.\r
+* Bugfix: Fix several other "undefined index" errors at various locations on the plugin's settings screen.\r
+* Tested for compatibility with WordPress 4.3.\r
+\r
+= Version 0.8.3 =\r
+\r
+* [Feature](https://wordpress.org/support/topic/feature-request-send-excerpt-image?replies=17#post-6665842): When importing a Photo post from Tumblr, the photo in the Tumblr post becomes the Featured Image of the WordPress post. This only happens when a Tumblr post contains a single photo.\r
+* Compatibility with WordPress 4.2.x's new PressThis bookmarklet.\r
+\r
+= Version 0.8.2 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/link-failure): Typo caused "View post on Tumblr" buttons to break. My bad. ^_^;\r
+\r
+= Version 0.8.1 =\r
+\r
+* Feature: Support [`rel-syndication` IndieWeb pattern](https://indiewebcamp.com/rel-syndication) as implemented by the recommended [Syndication Links](https://indiewebcamp.com/rel-syndication#How_to_link_from_WordPress) plugin.\r
+ * `rel-syndication` is an IndieWeb best practice recommendation that provides a way to automatically link to crossposted copies (called "POSSE'd copies" in the jargon) of your posts to improve the discoverability and usability of your posts. For Tumblr Crosspostr's `rel-syndication` to work, you must also install a compatible WordPress syndication links plugin, such as the [Syndication Links](https://wordpress.org/plugins/syndication-links/) plugin, but the absence of such a plugin will not cause any problems, either.\r
+\r
+= Version 0.8 =\r
+\r
+* Feature: Option to cross-post any [post type](https://codex.wordpress.org/Post_Type). Not all post types can be crossposted safely, but many can, especially if they use default WordPress features like "title" and "excerpt" and so on. On important websites, don't enable crossposting for post types whose compatibility with Tumblr you are not sure of, or at least make sure you have a backup you can restore from. :)\r
+* Developer: Three new filter hooks allow you to create your own custom post types that will be sent to Tumblr:\r
+ * Use the new `tumblr_crosspostr_save_post_types` filter hook to programmatically add custom post types to be processed by Tumblr Crosspostr during WordPress's `save_post` action.\r
+ * Use the new `tumblr_crosspostr_meta_box_post_types` filter hook to programmatically add or remove the Tumblr Crosspostr post editing meta box from certain post types.\r
+ * Use the new `tumblr_crosspostr_prepared_post` filter hook to programmatically alter the `$prepared_post` object immediately before it is crossposted to Tumblr.\r
+* Bugfix: First-time sync's now import all intended posts even when some posts are not public on Tumblr. Additionally, much-improved debug logging offers an easier way to trace sync problems.\r
+* Bugfix: Repeated sync's no longer cause duplicated posts on PHP less than 5.4.\r
+\r
+= Version 0.7.24 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/photosetmultiple-images-problem): Importing Photosets no longer duplicates the post caption for each image. Additionally, each photo's individual caption is correctly added to the `<img>` element's `alt` attribute.\r
+\r
+= Version 0.7.23 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/crosspostr-ignores-filters): Avoid crossposting loops during Tumblr sync when posts are created through automatic publication.\r
+* Usability: Improve error detail when "detailed debugging" is enabled.\r
+\r
+= Version 0.7.22 =\r
+\r
+* [Feature](https://wordpress.org/support/topic/sync-from-tumblr-define-category): Automatically assign categories to posts sync'ed from Tumblr. (Reminder: the sync feature is still experimental. Use with some caution.)\r
+* Feature: Set a click-thru link for an Image/Photo post. Whatever you tell WordPress to "Link To" (in its native image editing tool) is the URL Tumblr Crosspostr will use for the click-thru link on Tumblr.\r
+\r
+= Version 0.7.21 =\r
+\r
+* [Bugfix](https://github.com/meitar/tumblr-crosspostr/issues/6): Correctly process posts published using "Press This" bookmarklet (and numerous other tools).\r
+* Bugfix: Admin notices now output valid HTML.\r
+\r
+= Version 0.7.20 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/featured-image-metabox-hidden-on-page-edit-screens#post-6414152): Fix conflict with theme support for [Post Thumbnails](https://codex.wordpress.org/Post_Thumbnails) on `page` post types.\r
+\r
+= Version 0.7.19 =\r
+\r
+* [Feature](https://github.com/meitar/tumblr-crosspostr/issues/15): Rudimentary support for Featured Images on posts. When crossposting a Photo post, if no `<img>` is found in the post body, checks to see if a Featured Image is set and uses it instead.\r
+* Officially compatible with [WordPress Version 4.1](https://codex.wordpress.org/Version_4.1).\r
+\r
+= Version 0.7.18 =\r
+\r
+* [Developer](https://wordpress.org/support/topic/get-reblog-key-from-created-post): New action hook `tumblr_crosspostr_reblog_key` (calling template tag `tumblr_reblog_key`) now enables theme developers to create Tumblr reblog buttons for all crossposted posts. To print a crossposted post's Tumblr reblog key, use the following code inside The Loop:\r
+ * `<?php do_action('tumblr_crosspostr_reblog_key');?>`\r
+\r
+= Version 0.7.17 =\r
+\r
+* Feature: Show "View post on Tumblr" link in Post Edit screen inside Tumblr Crosspostr Custom Metabox. Useful for reviewing individual crossposted entries and ensuring WordPress and Tumblr are still in sync.\r
+* Bugfix: Clear WP-Cron schedules on plugin deactivation. (This improves performance, security, and prevents errors by ensuring any Tumblr synchronization routines are not invoked if you have deactivated but not deleted Tumblr Crosspostr.)\r
+\r
+= Version 0.7.16 =\r
+\r
+* [Feature](https://wordpress.org/support/topic/feature-request-tumblr-post-link): Show "View post on Tumblr" link in Posts listing screen.\r
+\r
+= Version 0.7.15 =\r
+\r
+* [Feature](https://wordpress.org/support/topic/twitter-toggle): Option to set global default for "Send tweet?" Useful for multi-author blogs and customized editorial workflows. (You can still override this on a per-post basis.)\r
+\r
+= Version 0.7.14 =\r
+\r
+* [Bugfix](https://github.com/meitar/tumblr-crosspostr/issues/7): Actually fix slashes in tweets.\r
+* Tested with version 3.9.1.\r
+\r
+= Version 0.7.13 =\r
+\r
+* [Bugfix](https://github.com/meitar/tumblr-crosspostr/issues/7): Correctly post tweets that have quotation marks without slashes in them. (Also fixes the case where a tweet would not be posted because the added slashes pushed the tweet contents over Twitter's 140 character length limit.)\r
+\r
+= Version 0.7.12 =\r
+\r
+* [Bugfix](https://github.com/meitar/tumblr-crosspostr/issues/8): Treat `tumblr_post_id` meta field value as string (not integer) to prevent 32 bit systems from overflowing and attempting to edit posts with a different ID than stored in the database.\r
+\r
+= Version 0.7.11 =\r
+\r
+* [Bugfix](https://wordpress.org/support/topic/error-400-403): Fix "400 Bad Request" errors on attempts to crosspost large amounts of data.\r
+* [Bugfix](https://wordpress.org/support/topic/invalid-argument-supplied-7): Fix "Invalid Argument Supplied" error when connectivity to Tumblr is flaky.\r
+\r
+= Version 0.7.10 =\r
+\r
+* Feature: "Sync posts from Tumblr" now imports audio files as attachments and displays them in WordPress's HTML5 player.\r
+\r
+= Version 0.7.9 =\r
+\r
+* Security: Improved protection for OAuth access tokens.\r
+* Bugfix: Ensure sanitization routines do not corrupt OAuth access tokens.\r
+* Minor code cleanup.\r
+\r
+= Version 0.7.8.5 =\r
+\r
+* Bugfix: Save value of "Send excerpt instead of main content?" option locally even if not sending a crosspost.\r
+* Bugfix: Correct `title` value in "Send excerpt instead of main content?" option so tooltip help text is actually helpful.\r
+* Minor code cleanup.\r
+\r
+= Version 0.7.8.4 =\r
+\r
+* Troubleshooting: New "Enable detailed debugging information?" option shows you a lot more information about errors. Use in conjunction with WordPress's built-in [`WP_DEBUG`](https://codex.wordpress.org/Debugging_in_WordPress#WP_DEBUG) and [`WP_DEBUG_LOG`](https://codex.wordpress.org/Debugging_in_WordPress#WP_DEBUG_LOG) to get even more information in your `wp-content/debug.log` file.\r
+\r
+= Version 0.7.8.3 =\r
+\r
+* Improved error handling:\r
+ * Tumblr Crosspostr will tell you if crossposting fails, and will suggest possible troubleshooting steps to help you resolve the issue.\r
+* Bugfix: Restore "Send this post to Tumblr?" field legend in meta box.\r
+\r
+= Version 0.7.8.2 =\r
+\r
+* Bugfix: Import media even when response code headers are not strictly numeric.\r
+\r
+= Version 0.7.8.1 =\r
+\r
+* Bugfix: Fix [fatal error on activation for some PHP installations](https://wordpress.org/support/topic/fatal-error-on-activation-31?replies=1#post-5318136).\r
+\r
+= Version 0.7.8 =\r
+\r
+* Feature: "Sync posts from Tumblr" now downloads the images in your Photo posts on Tumblr as [WordPress attachments](http://codex.wordpress.org/Attachments) and associates them with the newly-imported post (unless your WordPress uploads directory is not writable).\r
+\r
+= Version 0.7.7 =\r
+\r
+* Audio posts got better:\r
+ * In addition to `mp3` files, `wav`, `wma`, `aiff`, `ogg`, `ra`, `ram`, `rm`, `mid`, `alac`, and `flac` audio files are now crossposted, too.\r
+\r
+= Version 0.7.6.1 =\r
+\r
+* Video posts got *even better*:\r
+ * Videos from any source can be crossposted now, too. Just make sure the embed code the video site gives you is using an `<iframe>`. [The Onion](http://TheOnion.com/) fans, this one's for you! ;)\r
+\r
+= Version 0.7.6 =\r
+\r
+* Video posts got better:\r
+ * Vimeo embeds are now crossposted, too.\r
+ * YouTube's "privacy-enhanced" (`nocookie`) mode is now supported, so help protect your readers' privacy by embedding privacy-enhanced YouTube videos on your blog. [Quoth Teh Googlez](https://support.google.com/youtube/answer/171780?expand=PrivacyEnhancedMode#privacy): "Enabling this option means that YouTube won't store information about visitors on your web page unless they play the video." ([Learn more about why this matters](http://maymay.net/blog/2014/03/01/advertisements-are-malware/).)\r
+* Feature: WordPress post slugs become Tumblr custom post slugs (for Tumblr blogs with that feature enabled).\r
+* Developer: Replace [PEAR's `HTTP_OAuth`](https://pear.php.net/package/HTTP_OAuth) with [Manuel Lemos's `oauth_client_class`](https://freecode.com/projects/php-oauth-api). This is a major under-the-hood update that makes it easier for Tumblr Crosspostr's codebase to be reused with other [OAuth](http://oauth.net/) Web services. It also happens to reduce the plugin's total disk space used by about half. :)\r
+* Bugfix: Ampersands (`&`) in crossposted tags now display correctly on Tumblr.\r
+\r
+= Version 0.7.5 =\r
+\r
+* Bugfix: Updating a previously published post will keep the publication date set on the original WordPress post when sending to Tumblr.\r
+\r
+= Version 0.7.4.1 =\r
+\r
+* Bugfix: When making a "Link" post and sending the post excerpt instead of the main content to Tumblr, the link in your excerpt is used as the featured link (if it has one) rather than the link in your main content.\r
+\r
+= Version 0.7.4 =\r
+\r
+* Feature: "Send excerpt instead of main content?" option lets you crosspost a post's [excerpt](http://codex.wordpress.org/Excerpt) rather than its main content. If you use this option but do not provide an excerpt manually, an automatic one is generated (similar to how the [`the_excerpt()`](http://codex.wordpress.org/Function_Reference/the_excerpt) template tag works).\r
+* Feature: When you publish a post, a "View post on Tumblr" link offers an easy way to see your crossposted entry.\r
+\r
+= Version 0.7.3 =\r
+\r
+* Feature: Customize the auto-tweet when publishing a new post. This only works if the Tumblr blog you're crossposting to is already connected to a Twitter account. [Tumblr's documentation explains how to connect your Tumblr blog to your Twitter account](http://www.tumblr.com/docs/twitter).\r
+\r
+= Version 0.7.2 =\r
+\r
+* Feature: Save a given post's reblog key when importing that post with the "Sync posts from Tumblr" feature. Theme authors can then use the `tumblr_reblog_key` [custom field](http://codex.wordpress.org/Custom_Fields) to create a link on the WordPress post that lets a user reblog the original post on Tumblr. For instance:\r
+ * `<a href="http://www.tumblr.com/reblog/<?php echo get_post_meta($post->ID, 'tumblr_post_id', true);?>/<?php echo get_post_meta($post->ID, 'tumblr_reblog_key', true);?>?redirect_to=<?php echo esc_url(get_permalink());?>">`\r
+* Feature: Manual Tumblr disconnection button. If you want to change the Tumblr account or OAuth application credentials used to connect to Tumblr after you made a prior connection, you can now use the "Disconnect" button to disestablish your existing connection and create it anew.\r
+\r
+= Version 0.7.1 =\r
+\r
+* Bugfix: Use PHP's `__FILE__` constant instead of `__DIR__` to support PHP 5.2.x installations.\r
+\r
+= Version 0.7 =\r
+\r
+* Feature: "Sync posts from Tumblr" will import posts you create on your Tumblr blog(s) into your WordPress blog, along with their metadata such as tags, post types/formats, and content sources. This is useful for creating an automatic backup of the conversations you have in reblog threads on Tumblr.\r
+ * When first activated, your entire Tumblr archive will be copied (including private posts).\r
+ * Once every 24 hours, Tumblr Crosspostr will fetch up to the most recent 100 posts on your Tumblr blog to see if you have reblogged anything on Tumblr. If you have, Tumblr Crosspostr will import those posts to your WordPress blog.\r
+ * Posts you created on Tumblr using Tumblr Crosspostr will not be duplicated.\r
+ * Once imported to WordPress, edits you make on Tumblr are not retrieved, but edits you make on WordPress are sent back to Tumblr, so prefer using WordPress to edit and update your imported posts.\r
+ * **This feature is experimental.** Please make sure you have a backup of your WordPress website before you enable sync'ing from Tumblr.\r
+\r
+= Version 0.6.2 =\r
+\r
+* French translation (`fr_FR`). Activate the French version of this plugin by [configuring your WordPress to use that language](http://codex.wordpress.org/WordPress_in_Your_Language). Want Tumblr Crosspostr in your language? [Help us translate](https://www.transifex.com/signup/contributor/?next=/projects/p/tumblr-crosspostr/)!\r
+* Bugfix: Contextual help's support and donation links now open in new tabs or windows so you don't lose your place when writing.\r
+\r
+= Version 0.6.1 = \r
+\r
+* Feature: Detailed help is now available from the WordPress post screen's "Help" tab.\r
+\r
+= Version 0.6 =\r
+\r
+* Feature: "Tumblrize Archives" tool crossposts all post archives to Tumblr. This feature respects individual post settings, if there are any already set. This is especially useful as a one-time operation when you first install Tumblr Crosspostr to quickly crosspost your existing blog over to Tumblr. Note that this is not a "Sync" button in that it cannot delete posts from Tumblr that have already been deleted from WordPress (because those posts no longer exist in WordPress). To delete posts from WordPress and Tumblr in batch, use WordPress's standard "Move to trash" buttons.\r
+\r
+= Version 0.5.1 =\r
+\r
+* Bugfix: Correctly capture all lines in a multiple-line `<blockquote>`.\r
+\r
+= Version 0.5 =\r
+\r
+* Feature: Automatically register supported post formats. This means you can now use Tumblr Crosspostr even with themes that do not natively support the post format you want.\r
+* Bugfix: Don't try cross-posting if we don't have an API connection to Tumblr.\r
+\r
+= Version 0.4 =\r
+\r
+* "Quote" posts got better:\r
+ * Fixes bug where certain `<blockquote>` code caused failure to post to Tumblr.\r
+ * Start your WordPress post with a `<blockquote>` and everything in your post after the first `</blockquote>` will be used for the "Source" field on Tumblr, including HTML.\r
+\r
+= Version 0.3 =\r
+\r
+* Feature: Implement support for [Tumblr's meta "Content source" field](http://staff.tumblr.com/post/1059624418/content-attribution). Use the `Content source` field in Tumblr Crosspostr's post editing box to set the "Content source" field of your post on Tumblr.\r
+* Security: Harden HTML placeholder replacement subroutine.\r
+* Other minor improvements fix several PHP `E_NOTICE` messages on extremely sensitive systems.\r
+\r
+= Version 0.2 =\r
+\r
+* Feature: Implement support for `Chat` post format and type. Simply write your WordPress chat post the way Tumblr expects, one remark per line with speaker labels, like this:\r
+\r
+ Person A: Some inane observation.\r
+ Person B: Some witty retort.\r
+\r
+= Verson 0.1 =\r
+\r
+* Initial release.\r
+\r
+== Other notes ==\r
+\r
+Maintaining this plugin is a labor of love. However, if you like it, please consider [making a donation](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted) for your use of the plugin, [purchasing one of Meitar's web development books](http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2Fs%3Fie%3DUTF8%26redirect%3Dtrue%26sort%3Drelevancerank%26search-type%3Dss%26index%3Dbooks%26ref%3Dntt%255Fathr%255Fdp%255Fsr%255F2%26field-author%3DMeitar%2520Moscovitz&tag=maymaydotnet-20&linkCode=ur2&camp=1789&creative=390957) or, better yet, contributing directly to [Meitar's Cyberbusking fund](http://Cyberbusking.org/). (Publishing royalties ain't exactly the lucrative income it used to be, y'know?) Your support is appreciated!\r
+\r
+= Developer reference =\r
+\r
+Tumblr Crosspostr provides the following hooks for plugin and theme authors:\r
+\r
+*Filters*\r
+\r
+* `tumblr_crosspostr_save_post_types` - Filter an array of custom post type names to process when Tumblr Crosspostr is invoked in the `save_post` WordPress action.\r
+* `tumblr_crosspostr_meta_box_post_types` - Filter an array of custom post type names for which to show the Tumblr Crosspostr post editing meta box.\r
+* `tumblr_crosspostr_prepared_post` - Filter the `$prepared_post` object immediately before it gets crossposted to Tumblr\r
+\r
+*Actions*\r
+\r
+* `tumblr_crosspostr_reblog_key` - Prints the Tumblr Reblog Key for a given post.\r
--- /dev/null
+<?php
+/**
+ * Tumblr Crosspostr Template Tags
+ *
+ * @package Plugin
+ */
+
+function tumblr_reblog_key($post_id = false) {
+ global $post, $tumblr_crosspostr;
+ if (empty($post_id)) {
+ $post_id = $post->ID;
+ }
+ print $tumblr_crosspostr->getReblogKey($post_id);
+}
--- /dev/null
+.settings_page_tumblr_crosspostr_settings legend {
+ font-size: large;
+ font-style: italic;
+}
+.settings_page_tumblr_crosspostr_settings fieldset:target {
+ border: 2px solid blue;
+ padding: 0 15px 15px 15px;
+ margin-bottom: 2em;
+}
+.settings_page_tumblr_crosspostr_settings .fieldset-toc,
+.settings_page_tumblr_crosspostr_settings .fieldset-toc li {
+ display: inline;
+}
+.settings_page_tumblr_crosspostr_settings .fieldset-toc li:not(:last-child)::after {
+ content: " | ";
+}
+.settings_page_tumblr_crosspostr_settings fieldset {
+ margin-top: 1em;
+}
+#tumblr-crosspostr-meta-box legend {
+ display: none;
+}
+#tumblr-crosspostr-meta-box input[type="text"] {
+ width: 100%;
+}
--- /dev/null
+<?php
+/**
+ * Plugin Name: Tumblr Crosspostr
+ * Plugin URI: https://github.com/meitar/tumblr-crosspostr/#readme
+ * Description: Automatically crossposts to your Tumblr blog when you publish a post on your WordPress blog.
+ * Version: 0.8.4
+ * Author: Meitar Moscovitz
+ * Author URI: http://Cyberbusking.org/
+ * Text Domain: tumblr-crosspostr
+ * Domain Path: /languages
+ */
+
+class Tumblr_Crosspostr {
+ private $tumblr; //< Tumblr API manipulation wrapper.
+ private $prefix = 'tumblr_crosspostr'; //< String to prefix plugin options, settings, etc.
+
+ public function __construct () {
+ add_action('plugins_loaded', array($this, 'registerL10n'));
+ add_action('init', array($this, 'updateChangedSettings'));
+ add_action('init', array($this, 'setSyncSchedules'));
+ add_action('admin_init', array($this, 'registerSettings'));
+ add_action('admin_menu', array($this, 'registerAdminMenu'));
+ add_action('admin_enqueue_scripts', array($this, 'registerAdminScripts'));
+ add_action('admin_head', array($this, 'registerContextualHelp'));
+ add_action('admin_notices', array($this, 'showAdminNotices'));
+ add_action('add_meta_boxes', array($this, 'addMetaBox'));
+ add_action('save_post', array($this, 'savePost'));
+ add_action('before_delete_post', array($this, 'removeFromTumblr'));
+ // run late, so themes have a chance to register support
+ add_action('after_setup_theme', array($this, 'registerThemeSupport'), 700);
+
+ add_action($this->prefix . '_sync_content', array($this, 'syncFromTumblrBlog'));
+
+ // Template tag actions
+ add_action($this->prefix . '_reblog_key', 'tumblr_reblog_key');
+
+ add_filter('post_row_actions', array($this, 'addPostRowAction'), 10, 2);
+ add_filter('plugin_row_meta', array($this, 'addPluginRowMeta'), 10, 2);
+ add_filter('syn_add_links', array($this, 'addSyndicatedLinks'));
+
+ register_deactivation_hook(__FILE__, array($this, 'deactivate'));
+
+ $options = get_option($this->prefix . '_settings');
+ // Initialize consumer if we can, set up authroization flow if we can't.
+ require_once 'lib/TumblrCrosspostrAPIClient.php';
+ if (isset($options['consumer_key']) && isset($options['consumer_secret'])) {
+ $this->tumblr = new Tumblr_Crosspostr_API_Client($options['consumer_key'], $options['consumer_secret']);
+ if (get_option($this->prefix . '_access_token') && get_option($this->prefix . '_access_token_secret')) {
+ $this->tumblr->client->access_token = get_option($this->prefix . '_access_token');
+ $this->tumblr->client->access_token_secret = get_option($this->prefix . '_access_token_secret');
+ }
+ } else {
+ $this->tumblr = new Tumblr_Crosspostr_API_Client;
+ add_action('admin_notices', array($this, 'showMissingConfigNotice'));
+ }
+
+ if (isset($options['debug'])) {
+ $this->tumblr->client->debug = 1;
+ $this->tumblr->client->debug_http = 1;
+ }
+
+ // OAuth connection workflow.
+ if (isset($_GET[$this->prefix . '_oauth_authorize'])) {
+ add_action('init', array($this, 'authorizeApp'));
+ } else if (isset($_GET[$this->prefix . '_callback']) && !empty($_GET['oauth_verifier'])) {
+ // Unless we're just saving the options, hook the final step in OAuth authorization.
+ if (!isset($_GET['settings-updated'])) {
+ add_action('init', array($this, 'completeAuthorization'));
+ }
+ }
+ }
+
+ // Detects old options/settings and migrates them to current version.
+ public function updateChangedSettings () {
+ $options = get_option($this->prefix . '_settings');
+ if (false === $options) { return; } // don't need to migrate anything
+ $new_opts = array();
+ foreach ($options as $opt_name => $opt_value) {
+ switch ($opt_name) {
+ // Rename "sync_tumblr" to "sync_content"
+ case 'sync_tumblr':
+ foreach ($opt_value as $blog_to_sync) {
+ $new_opts['sync_content'][] = $blog_to_sync;
+ // Remove events with old name.
+ wp_unschedule_event(
+ wp_next_scheduled($this->prefix . '_sync_tumblr', array($blog_to_sync)),
+ $this->prefix . '_sync_tumblr',
+ array($blog_to_sync)
+ );
+ }
+ break;
+ // Move the OAuth access tokens to their own options.
+ case 'access_token':
+ case 'access_token_secret':
+ update_option($this->prefix . '_' . $opt_name, $opt_value);
+ break;
+ default:
+ // Do nothing for the rest, they're unchanged.
+ $new_opts[$opt_name] = $opt_value;
+ break;
+ }
+ }
+ update_option($this->prefix . '_settings', $new_opts);
+ }
+
+ /**
+ * Implements rel-syndication for POSSE as expected by the
+ * Syndication Links plugin for WordPress.
+ *
+ * @see https://wordpress.org/plugins/syndication-links/
+ * @see http://indiewebcamp.com/rel-syndication
+ */
+ public function addSyndicatedLinks ($urls) {
+ $urls[] = $this->getSyndicatedAddress(get_the_id());
+ return $urls;
+ }
+
+ public function showMissingConfigNotice () {
+ $screen = get_current_screen();
+ if ($screen->base === 'plugins') {
+?>
+<div class="updated">
+ <p><a href="<?php print admin_url('options-general.php?page=' . $this->prefix . '_settings');?>" class="button"><?php esc_html_e('Connect to Tumblr', 'tumblr-crosspostr');?></a> — <?php esc_html_e('Almost done! Connect your blog to Tumblr to begin crossposting with Tumblr Crosspostr.', 'tumblr-crosspostr');?></p>
+</div>
+<?php
+ }
+ }
+
+ private function showError ($msg) {
+?>
+<div class="notice is-dismissible error">
+ <p><?php print esc_html($msg);?></p>
+</div>
+<?php
+ }
+
+ private function showNotice ($msg) {
+?>
+<div class="notice is-dismissible updated">
+ <p><?php print strip_tags($msg, '<a>');?></p>
+</div>
+<?php
+ }
+
+ private function showDonationAppeal () {
+?>
+<div class="donation-appeal">
+ <p style="text-align: center; font-size: larger; width: 70%; margin: 0 auto;"><?php print sprintf(
+esc_html__('Tumblr Crosspostr is provided as free software, but sadly grocery stores do not offer free food. If you like this plugin, please consider %1$s to its %2$s. ♥ Thank you!', 'tumblr-crosspostr'),
+'<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">' . esc_html__('making a donation', 'tumblr-crosspostr') . '</a>',
+'<a href="http://Cyberbusking.org/">' . esc_html__('houseless, jobless, nomadic developer', 'tumblr-crosspostr') . '</a>'
+);?></p>
+</div>
+<?php
+ }
+
+ public function registerThemeSupport () {
+ add_theme_support(
+ 'post-formats',
+ $this->diffThemeSupport(array(
+ 'link',
+ 'image',
+ 'quote',
+ 'video',
+ 'audio',
+ 'chat'
+ ), 'post-formats')
+ );
+ }
+
+ /**
+ * Returns the difference between a requested and existing theme support for a feature.
+ *
+ * @param array $new_array The options of a feature to query.
+ * @param string $feature The feature to query.
+ * @return array The difference, each element as an argument to original add_theme_support() call.
+ */
+ private function diffThemeSupport ($new_array, $feature) {
+ $x = get_theme_support($feature);
+ if (is_bool($x)) { $x = array(); }
+ $y = (empty($x)) ? array() : $x[0];
+ return array_merge($y, array_diff($new_array, $y));
+ }
+
+ public function authorizeApp () {
+ check_admin_referer('tumblr-authorize');
+ $this->tumblr->authorize(admin_url('options-general.php?page=' . $this->prefix . '_settings&' . $this->prefix . '_callback'));
+ }
+
+ public function completeAuthorization () {
+ $tokens = $this->tumblr->completeAuthorization(admin_url('options-general.php?page=' . $this->prefix . '_settings&' . $this->prefix . '_callback'));
+ update_option($this->prefix . '_access_token', $tokens['value']);
+ update_option($this->prefix . '_access_token_secret', $tokens['secret']);
+ }
+
+ public function registerL10n () {
+ load_plugin_textdomain('tumblr-crosspostr', false, dirname(plugin_basename(__FILE__)) . '/languages/');
+ }
+
+ public function registerSettings () {
+ register_setting(
+ $this->prefix . '_settings',
+ $this->prefix . '_settings',
+ array($this, 'validateSettings')
+ );
+ }
+
+ public function registerContextualHelp () {
+ $screen = get_current_screen();
+ if ($screen->id !== 'post') { return; }
+ $html = '<p>' . esc_html__('You can automatically copy this post to your Tumblr blog:', 'tumblr-crosspostr') . '</p>'
+ . '<ol>'
+ . '<li>' . sprintf(
+ esc_html__('Compose your post for WordPress as you normally would, with the appropriate %sPost Format%s.', 'tumblr-crosspostr'),
+ '<a href="#formatdiv">', '</a>'
+ ) . '</li>'
+ . '<li>' . sprintf(
+ esc_html__('In %sthe Tumblr Crosspostr box%s, ensure the "Send this post to Tumblr?" option is set to "Yes." (You can set it to "No" if you do not want to copy this post to Tumblr.)', 'tumblr-crosspostr'),
+ '<a href="#tumblr-crosspostr-meta-box">', '</a>'
+ ) . '</li>'
+ . '<li>' . esc_html__('If you have more than one Tumblr, choose the one you want to send this post to from the "Send to my Tumblr blog" list.', 'tumblr-crosspostr') . '</li>'
+ . '<li>' . esc_html__('Optionally, enter any additional details specifically for Tumblr, such as the "Content source" field.', 'tumblr-crosspostr') . '</li>'
+ . '</ol>'
+ . '<p>' . esc_html__('When you are done, click "Publish" (or "Save Draft"), and Tumblr Crosspostr will send your post to the Tumblr blog you chose.', 'tumblr-crosspostr') . '</p>'
+ . '<p>' . esc_html__('Note that Tumblr does not allow you to change the post format after you have saved a copy of your post, so please be sure you choose the appropriate Post Format before you save your post.', 'tumblr-crosspostr') . '</p>';
+ ob_start();
+ $this->showDonationAppeal();
+ $x = ob_get_contents();
+ ob_end_clean();
+ $html .= $x;
+ $screen->add_help_tab(array(
+ 'id' => $this->prefix . '-' . $screen->base . '-help',
+ 'title' => __('Crossposting to Tumblr', 'tumblr-crosspostr'),
+ 'content' => $html
+ ));
+
+ $x = esc_html__('Tumblr Crosspostr:', 'tumblr-crosspostr');
+ $y = esc_html__('Tumblr Crosspostr support forum', 'tumblr-crosspostr');
+ $z = esc_html__('Donate to Tumblr Crosspostr', 'tumblr-crosspostr');
+ $sidebar = <<<END_HTML
+<p><strong>$x</strong></p>
+<p><a href="https://wordpress.org/support/plugin/tumblr-crosspostr" target="_blank">$y</a></p>
+<p><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted" target="_blank">♥ $z ♥</a></p>
+END_HTML;
+ $screen->set_help_sidebar($screen->get_help_sidebar() . $sidebar);
+ }
+
+ private function getSyndicatedAddress ($post_id) {
+ $url = '';
+ if ($id = get_post_meta($post_id, 'tumblr_post_id', true)) {
+ $base_hostname = $this->getTumblrBasename($post_id);
+ $url .= "http://{$base_hostname}/post/{$id}";
+ }
+ return $url;
+ }
+
+ public function addPostRowAction ($actions, $post) {
+ $id = get_post_meta($post->ID, 'tumblr_post_id', true);
+ if ($id) {
+ $actions['view_on_tumblr'] = '<a href="' . $this->getSyndicatedAddress($post->ID) . '">' . esc_html__('View post on Tumblr', 'tumblr-crosspostr') . '</a>';
+ }
+ return $actions;
+ }
+
+ public function addPluginRowMeta ($links, $file) {
+ if (false !== strpos($file, basename(__FILE__))) {
+ $new_links = array(
+ '♥ <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=meitarm%40gmail%2ecom&lc=US&item_name=Tumblr%20Crosspostr%20WordPress%20Plugin&item_number=tumblr%2dcrosspostr&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted">' . esc_html__('Donate to Tumblr Crosspostr', 'tumblr-crosspostr') . '</a> ♥',
+ '<a href="https://wordpress.org/support/plugin/tumblr-crosspostr/">' . esc_html__('Tumblr Crosspostr support forum', 'tumblr-crosspostr') . '</a>'
+ );
+ $links = array_merge($links, $new_links);
+ }
+ return $links;
+ }
+
+ private function WordPressPostFormat2TumblrPostType ($format) {
+ switch ($format) {
+ case 'image':
+ case 'gallery':
+ $type = 'photo';
+ break;
+ case 'quote':
+ $type = 'quote';
+ break;
+ case 'link':
+ $type = 'link';
+ break;
+ case 'audio':
+ $type = 'audio';
+ break;
+ case 'video':
+ $type = 'video';
+ break;
+ case 'chat':
+ $type = 'chat';
+ break;
+ case 'aside':
+ case false:
+ default:
+ $type = 'text';
+ break;
+ }
+ return $type;
+ }
+
+ private function TumblrPostType2WordPressPostFormat ($type) {
+ switch ($type) {
+ case 'quote':
+ case 'audio':
+ case 'video':
+ case 'chat':
+ case 'link':
+ $format = $type;
+ break;
+ case 'photo':
+ $format = 'image';
+ break;
+ case 'answer':
+ case 'text':
+ default:
+ $format = '';
+ }
+ return $format;
+ }
+
+ /**
+ * Translates a WordPress post status to a Tumblr post state.
+ *
+ * @param string $status The WordPress post status to translate.
+ * @return mixed The translates Tumblr post state or false if the WordPress status has no equivalently compatible state on Tumblr.
+ */
+ private function WordPressStatus2TumblrState ($status) {
+ switch ($status) {
+ case 'draft':
+ case 'private':
+ $state = $status;
+ break;
+ case 'publish':
+ $state = 'published';
+ break;
+ case 'future':
+ $state = 'queue';
+ break;
+ case 'auto-draft':
+ case 'inherit':
+ case 'pending':
+ default:
+ $state = false;
+ }
+ return $state;
+ }
+
+ private function TumblrState2WordPressStatus ($state) {
+ switch ($state) {
+ case 'draft':
+ case 'private':
+ $status = $state;
+ break;
+ case 'queue':
+ $status = 'future';
+ case 'published':
+ default:
+ $status = 'publish';
+ }
+ return $status;
+ }
+
+ private function isPostCrosspostable ($post_id) {
+ $options = get_option($this->prefix . '_settings');
+ $crosspostable = true;
+
+ // Do not crosspost if this post is excluded by a certain category.
+ if (isset($options['exclude_categories']) && in_category($options['exclude_categories'], $post_id)) {
+ $crosspostable = false;
+ }
+
+ // Do not crosspost if this specific post was excluded.
+ if ('N' === get_post_meta($post_id, $this->prefix . '_crosspost', true)) {
+ $crosspostable = false;
+ }
+
+ // Do not crosspost unsupported post states.
+ if (!$this->WordPressStatus2TumblrState(get_post_status($post_id))) {
+ $crosspostable = false;
+ }
+
+ return $crosspostable;
+ }
+
+ /**
+ * Translates a WordPress post data for Tumblr's API.
+ *
+ * @param int $post_id The ID number of the WordPress post.
+ * @return mixed A simple object representing data for Tumblr or FALSE if the given post should not be crossposted.
+ */
+ private function prepareForTumblr ($post_id) {
+ if (!$this->isPostCrosspostable($post_id)) { return false; }
+
+ $options = get_option($this->prefix . '_settings');
+ $custom = get_post_custom($post_id);
+
+ $prepared_post = new stdClass();
+
+ // Set the post's Tumblr destination.
+ $base_hostname = false;
+ if (!empty($_POST[$this->prefix . '_destination'])) {
+ $base_hostname = sanitize_text_field($_POST[$this->prefix . '_destination']);
+ } else if (!empty($custom['tumblr_base_hostname'][0])) {
+ $base_hostname = sanitize_text_field($custom['tumblr_base_hostname'][0]);
+ } else {
+ $base_hostname = sanitize_text_field($options['default_hostname']);
+ }
+ if ($base_hostname !== $options['default_hostname']) {
+ update_post_meta($post_id, 'tumblr_base_hostname', $base_hostname);
+ }
+ $prepared_post->base_hostname = $base_hostname;
+
+ // Set "Content source" meta field.
+ if (!empty($_POST[$this->prefix . '_meta_source_url'])) {
+ $source_url = sanitize_text_field($_POST[$this->prefix . '_meta_source_url']);
+ update_post_meta($post_id, 'tumblr_source_url', $source_url);
+ } else if (!empty($custom['tumblr_source_url'][0])) {
+ $source_url = sanitize_text_field($custom['tumblr_source_url'][0]);
+ } else if ('Y' === $options['auto_source']) {
+ $source_url = get_permalink($post_id);
+ delete_post_meta($post_id, 'tumblr_source_url');
+ } else {
+ $source_url = false;
+ }
+
+ $format = (defined('DOING_AJAX') && DOING_AJAX) ? $_POST['post_format'] : get_post_format($post_id);
+ $state = $this->WordPressStatus2TumblrState(get_post_status($post_id));
+ $tags = array();
+ if ($t = get_the_tags($post_id)) {
+ foreach ($t as $tag) {
+ // Decode manually so that's the ONLY decoded entity.
+ $tags[] = str_replace('&', '&', $tag->name);
+ }
+ }
+ $common_params = array(
+ 'type' => $this->WordPressPostFormat2TumblrPostType($format),
+ 'state' => $state,
+ 'tags' => implode(',', $tags),
+ 'date' => get_post_time('Y-m-d H:i:s', true, $post_id) . ' GMT',
+ 'format' => 'html', // Tumblr's "formats" are always either 'html' or 'markdown'
+ 'slug' => get_post_field('post_name', $post_id)
+ );
+ if ($source_url) { $common_params['source_url'] = $source_url; }
+
+ if (!empty($options['exclude_tags'])) { unset($common_params['tags']); }
+
+ if (!empty($options['additional_tags'])) {
+ if (!isset($common_params['tags'])) {
+ $common_params['tags'] = '';
+ }
+ $common_params['tags'] = implode(',', array_merge(explode(',', $common_params['tags']), $options['additional_tags']));
+ }
+
+ $post_params = $this->prepareParamsByPostType($post_id, $common_params['type']);
+
+ if (!empty($options['additional_markup'])) {
+ $html = $this->replacePlaceholders($options['additional_markup'], $post_id);
+ foreach ($post_params as $k => $v) {
+ switch ($k) {
+ case 'body':
+ case 'caption':
+ case 'description':
+ $post_params[$k] = $v . $html; // append
+ break;
+ }
+ }
+ }
+
+ $prepared_post->params = array_merge($common_params, $post_params);
+
+ $tumblr_id = get_post_meta($post_id, 'tumblr_post_id', true); // Will be empty if none exists.
+ $prepared_post->tumblr_id = (empty($tumblr_id)) ? false : $tumblr_id;
+
+ return $prepared_post;
+ }
+
+ public function savePost ($post_id) {
+ if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; }
+ if (!$this->isConnectedToService()) { return; }
+
+ // Only crosspost regular posts unless asked to cross-post other types.
+ $options = get_option($this->prefix . '_settings');
+ $post_types = array('post');
+ if (!empty($options['post_types'])) {
+ $post_types = array_merge($post_types, $options['post_types']);
+ }
+ $post_types = apply_filters($this->prefix . '_save_post_types', $post_types);
+ if (!in_array(get_post_type($post_id), $post_types)) {
+ return;
+ }
+
+ if (isset($_POST[$this->prefix . '_use_excerpt'])) {
+ update_post_meta($post_id, $this->prefix . '_use_excerpt', 1);
+ } else {
+ delete_post_meta($post_id, $this->prefix . '_use_excerpt');
+ }
+
+ if (isset($_POST[$this->prefix . '_crosspost']) && 'N' === $_POST[$this->prefix . '_crosspost']) {
+ update_post_meta($post_id, $this->prefix . '_crosspost', 'N'); // 'N' means "no"
+ return;
+ } else {
+ delete_post_meta($post_id, $this->prefix . '_crosspost', 'N');
+ }
+
+ if ($prepared_post = $this->prepareForTumblr($post_id)) {
+ if (isset($_POST[$this->prefix . '_send_tweet'])) {
+ if (!empty($_POST[$this->prefix . '_tweet_text'])) {
+ $prepared_post->params['tweet'] = stripslashes_deep(sanitize_text_field($_POST[$this->prefix . '_tweet_text']));
+ }
+ } else {
+ $prepared_post->params['tweet'] = 'off';
+ }
+
+ $prepared_post = apply_filters($this->prefix . '_prepared_post', $prepared_post);
+ // We still have a post, right? in case someone forgets to return
+ if (empty($prepared_post)) { return; }
+
+ $data = $this->crosspostToTumblr($prepared_post->base_hostname, $prepared_post->params, $prepared_post->tumblr_id);
+ if (empty($data->response->id)) {
+ $msg = esc_html__('Crossposting to Tumblr failed.', 'tumblr-crosspostr');
+ if (isset($data->meta)) {
+ $msg .= esc_html__(' Remote service said:', 'tumblr-crosspostr');
+ $msg .= '<blockquote>';
+ $msg .= esc_html__('Response code:', 'tumblr-crosspostr') . " {$data->meta->status}<br />";
+ $msg .= esc_html__('Response message:', 'tumblr-crosspostr') . " {$data->meta->msg}<br />";
+ $msg .= '</blockquote>';
+ }
+ switch ($data->meta->status) {
+ case 401:
+ $msg .= ' ' . $this->maybeCaptureDebugOf($data);
+ $msg .= sprintf(
+ esc_html__('This might mean your %1$s are invalid or have been revoked by Tumblr. If everything looks fine on your end, you may want to ask %2$s to confirm your app is still allowed to use their API.', 'tumblr-crosspostr'),
+ '<a href="' . admin_url('options-general.php?page=' . $this->prefix . '_settings') . '">' . esc_html__('OAuth credentials', 'tumblr-crosspostr') . '</a>',
+ $this->linkToTumblrSupport()
+ );
+ break;
+ default:
+ $msg .= ' ' . $this->maybeCaptureDebugOf($data);
+ $msg .= sprintf(
+ esc_html__('Unfortunately, I have no idea what Tumblr is talking about. Consider asking %1$s for help. Tell them you are using %2$s, that you got the error shown above, and ask them to please support this tool. \'Cause, y\'know, it\'s not like you don\'t already have a WordPress blog, and don\'t they want you to use Tumblr, too?', 'tumblr-crosspostr'),
+ $this->linkToTumblrSupport(),
+ '<a href="https://wordpress.org/plugins/tumblr-crosspostr/">' . esc_html__('Tumblr Crosspostr', 'tumblr-crosspostr') . '</a>'
+ );
+ break;
+ }
+ if (!isset($options['debug'])) {
+ $msg .= '<br /><br />' . sprintf(
+ esc_html__('Additionally, you may want to turn on Tumblr Crosspostr\'s "%s" option to get more information about this error the next time it happens.', 'tumblr-crosspostr'),
+ '<a href="' . admin_url('options-general.php?page=' . $this->prefix . '_settings#' . $this->prefix . '_debug') . '">'
+ . esc_html__('Enable detailed debugging information?', 'tumblr-crosspostr') . '</a>'
+ );
+ }
+ $this->addAdminNotices($msg);
+ } else {
+ update_post_meta($post_id, 'tumblr_post_id', $data->response->id);
+ if ($prepared_post->params['state'] === 'published') {
+ $this->addAdminNotices(
+ esc_html__('Post crossposted.', 'tumblr-crosspostr') . ' <a href="' . $this->getSyndicatedAddress($post_id) . '">' . esc_html__('View post on Tumblr', 'tumblr-crosspostr') . '</a>'
+ );
+ if ($msg = $this->maybeCaptureDebugOf($data)) {
+ $msg = $this->maybeCaptureDebugOf($prepared_post) . '<br /><br />' . $msg;
+ $this->addAdminNotices($msg);
+ }
+ }
+ }
+ }
+ }
+
+ private function captureDebugOf ($var) {
+ ob_start();
+ var_dump($var);
+ $str = ob_get_contents();
+ ob_end_clean();
+ return $str;
+ }
+
+ private function maybeCaptureDebugOf ($var) {
+ $msg = '';
+ $options = get_option($this->prefix . '_settings');
+ if (isset($options['debug'])) {
+ $msg .= esc_html__('Debug output:', 'tumblr-crosspostr');
+ $msg .= '<pre>' . $this->captureDebugOf($var) . '</pre>';
+ }
+ return $msg;
+ }
+
+ private function linkToTumblrSupport () {
+ return '<a href="http://www.tumblr.com/help?form">' . esc_html__('Tumblr Support', 'tumblr-crosspostr') . '</a>';
+ }
+
+ private function addAdminNotices ($msgs) {
+ if (is_string($msgs)) { $msgs = array($msgs); }
+ $notices = get_option('_' . $this->prefix . '_admin_notices');
+ if (empty($notices)) {
+ $notices = array();
+ }
+ $notices = array_merge($notices, $msgs);
+ update_option('_' . $this->prefix . '_admin_notices', $notices);
+ }
+
+ public function showAdminNotices () {
+ $notices = get_option('_' . $this->prefix . '_admin_notices');
+ if ($notices) {
+ foreach ($notices as $msg) {
+ $this->showNotice($msg);
+ }
+ delete_option('_' . $this->prefix . '_admin_notices');
+ }
+ }
+
+ private function replacePlaceholders ($str, $post_id) {
+ $placeholders = array(
+ '%permalink%',
+ '%the_title%',
+ '%blog_url%',
+ '%blog_name%'
+ );
+ foreach ($placeholders as $x) {
+ if (0 === strpos($x, '%blog_')) {
+ $arg = substr($x, 6, -1);
+ $str = str_replace($x, get_bloginfo($arg), $str);
+ } else {
+ $func = 'get_' . substr($x, 1, -1);
+ $valid_funcs = array(
+ 'get_permalink',
+ 'get_the_title'
+ );
+ if (in_array($func, $valid_funcs, true)) {
+ $str = str_replace($x, call_user_func($func, $post_id), $str);
+ }
+ }
+ }
+ return $str;
+ }
+
+ /**
+ * Issues a Tumblr API call.
+ *
+ * @param string $blog The Tumblr blog's base hostname.
+ * @param array @params Any additional parameters for the request.
+ * @param int $tumblr_id The ID of a specific Tumblr post (only needed if editing or deleting this post).
+ * @param bool $deleting Whether or not to delete, rather than to edit, a specific Tumblr post.
+ * @return array Tumblr's decoded JSON response.
+ */
+ private function crosspostToTumblr ($blog, $params, $tumblr_id = false, $deleting = false) {
+ // TODO: Smoothen this deleting thing.
+ // Cancel WordPress deletions if Tumblr deletions aren't working?
+ if ($deleting === true && $tumblr_id) {
+ $params['id'] = $tumblr_id;
+ return $this->tumblr->deleteFromTumblrBlog($blog, $params);
+ } else if ($tumblr_id) {
+ $params['id'] = $tumblr_id;
+ return $this->tumblr->editOnTumblrBlog($blog, $params);
+ } else {
+ return $this->tumblr->postToTumblrBlog($blog, $params);
+ }
+ }
+
+ private function prepareParamsByPostType ($post_id, $type) {
+ $post_body = get_post_field('post_content', $post_id);
+ $post_excerpt = get_post_field('post_excerpt', $post_id);
+ // Mimic wp_trim_excerpt() without The Loop.
+ if (empty($post_excerpt)) {
+ $text = $post_body;
+ $text = strip_shortcodes($text);
+ $text = apply_filters('the_content', $text);
+ $text = str_replace(']]>', ']]>', $text);
+ $text = wp_trim_words($text);
+ $post_excerpt = $text;
+ }
+
+ $e = $this->getTumblrUseExcerpt($post_id); // Use excerpt?
+ $r = array();
+ switch ($type) {
+ case 'photo':
+ $r['caption'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', $this->strip_only($post_body, 'img', true, 1));
+ $pattern = '/(?:<a.*?href="(.*?)"[^>]*>)?<img.*?src="(.*?)".*?\/?>/';
+ $m = array();
+ preg_match($pattern, $post_body, $m);
+ if (empty($m)) {
+ $r['link'] = $r['source'] = $this->extractByRegex($pattern, get_the_post_thumbnail($post_id, 'full'), 2);
+ } else if (empty($m[1])) {
+ $r['link'] = $r['source'] = $m[2];
+ } else {
+ $r['link'] = $m[1];
+ $r['source'] = $m[2];
+ }
+ break;
+ case 'quote':
+ $pattern = '/<blockquote.*?>(.*?)<\/blockquote>/s';
+ $r['quote'] = wpautop($this->extractByRegex($pattern, $post_body, 1));
+ $len = strlen($this->extractByRegex($pattern, $post_body, 0));
+ $r['source'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', substr($post_body, $len));
+ break;
+ case 'link':
+ $r['title'] = strip_tags(get_post_field('post_title', $post_id));
+ $r['url'] = ($e && preg_match('/<a.*?href="(.*?)".*?>/', $post_excerpt))
+ ? $this->extractByRegex('/<a.*?href="(.*?)".*?>/', $post_excerpt, 1)
+ : $this->extractByRegex('/<a.*?href="(.*?)".*?>/', $post_body, 1);
+ $r['description'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', $post_body);
+ break;
+ case 'chat':
+ $r['title'] = strip_tags(get_post_field('post_title', $post_id));
+ $r['conversation'] = wp_strip_all_tags($post_body);
+ break;
+ case 'audio':
+ $r['caption'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', $post_body);
+ // Strip after apply_filters in case shortcode is used to generate <audio> element.
+ $r['caption'] = $this->strip_only($r['caption'], 'audio', 1);
+ $r['external_url'] = $this->extractByRegex('/(?:href|src)="(.*?\.(?:mp3|wav|wma|aiff|ogg|ra|ram|rm|mid|alac|flac))".*?>/i', $post_body, 1);
+ break;
+ case 'video':
+ $r['caption'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', $this->strip_only($post_body, 'iframe', true, 1));
+
+ $pattern_youtube = '/youtube(?:-nocookie)\.com\/(?:v|embed)\/([\w\-]+)/';
+ $pattern_vimeo = '/player\.vimeo\.com\/video\/([0-9]+)/';
+ if (preg_match($pattern_youtube, $post_body)) {
+ $r['embed'] = 'https://www.youtube.com/watch?v='
+ . $this->extractByRegex($pattern_youtube, $post_body, 1);
+ } else if (preg_match($pattern_vimeo, $post_body)) {
+ $r['embed'] = '<iframe src="//' . $this->extractByRegex($pattern_vimeo, $post_body, 0) . '">'
+ . '<a href="//vimeo.com/' . $this->extractByRegex($pattern_vimeo, $post_body, 1). '">'
+ . esc_html__('Watch this video.', 'tumblr-crosspostr') . '</a></iframe>';
+ } else {
+ // Pass along the entirety of any unrecognized <iframe>.
+ $r['embed'] = $this->extractByRegex('/<iframe.*?<\/iframe>/', $post_body, 0);
+ }
+ break;
+ case 'text':
+ $r['title'] = strip_tags(get_post_field('post_title', $post_id));
+ // fall through
+ case 'aside':
+ default:
+ $r['body'] = ($e)
+ ? apply_filters('the_excerpt', $post_excerpt)
+ : apply_filters('the_content', $post_body);
+ break;
+ }
+ return $r;
+ }
+
+ // TODO: Add error handling for when the $post_body doesn't give
+ // us what we need to fulfill the Tumblr post type req's.
+ /**
+ * Extracts a given string from another string according to a regular expression.
+ *
+ * @param string $pattern The PCRE-compatible regular expression.
+ * @param string $str The source from which to extract text matching the $pattern.
+ * @param int $group If the regex uses capture groups, the number of the capture group to return.
+ * @return string The matched text.
+ */
+ private function extractByRegex ($pattern, $str, $group = 0) {
+ $matches = array();
+ $x = preg_match($pattern, $str, $matches);
+ return (!empty($matches[$group])) ? $matches[$group] : $x;
+ }
+
+ private function getTumblrBasename ($post_id) {
+ $d = get_post_meta($post_id, 'tumblr_base_hostname', true);
+ if (empty($d)) {
+ $options = get_option($this->prefix . '_settings');
+ $d = (isset($options['default_hostname'])) ? $options['default_hostname'] : '';
+ }
+ return $d;
+ }
+
+ private function getTumblrUseExcerpt ($post_id) {
+ $e = get_post_meta($post_id, $this->prefix . '_use_excerpt', true);
+ if (empty($e)) {
+ $options = get_option($this->prefix . '_settings');
+ $e = (isset($options['use_excerpt'])) ? $options['use_excerpt'] : 0;
+ }
+ return intval($e);
+ }
+
+ public function removeFromTumblr ($post_id) {
+ $options = get_option($this->prefix . '_settings');
+ $tumblr_id = get_post_meta($post_id, 'tumblr_post_id', true);
+ $this->crosspostToTumblr($this->getTumblrBasename($post_id), array(), $tumblr_id, true);
+ }
+
+ public function getReblogKey ($post_id) {
+ $reblog_key = get_post_meta($post_id, 'tumblr_reblog_key', true);
+ if (empty($reblog_key)) {
+ $tumblr_id = get_post_meta($post_id, 'tumblr_post_id', true);
+ $options = get_option($this->prefix . '_settings');
+ $this->tumblr->setApiKey($options['consumer_key']);
+ $resp = $this->tumblr->getPosts($this->getTumblrBaseName($post_id), array('id' => $tumblr_id));
+ $reblog_key = $resp->posts[0]->reblog_key;
+ update_post_meta($post_id, 'tumblr_reblog_key', $reblog_key);
+ }
+ return $reblog_key;
+ }
+
+ /**
+ * @param array $input An array of of our unsanitized options.
+ * @return array An array of sanitized options.
+ */
+ public function validateSettings ($input) {
+ $safe_input = array();
+ foreach ($input as $k => $v) {
+ switch ($k) {
+ case 'consumer_key':
+ if (empty($v)) {
+ $errmsg = __('Consumer key cannot be empty.', 'tumblr-crosspostr');
+ add_settings_error($this->prefix . '_settings', 'empty-consumer-key', $errmsg);
+ }
+ $safe_input[$k] = sanitize_text_field($v);
+ break;
+ case 'consumer_secret':
+ if (empty($v)) {
+ $errmsg = __('Consumer secret cannot be empty.', 'tumblr-crosspostr');
+ add_settings_error($this->prefix . '_settings', 'empty-consumer-secret', $errmsg);
+ }
+ $safe_input[$k] = sanitize_text_field($v);
+ break;
+ case 'default_hostname':
+ $safe_input[$k] = sanitize_text_field($v);
+ break;
+ case 'sync_content':
+ $safe_input[$k] = array();
+ foreach ($v as $x) {
+ $safe_input[$k][] = sanitize_text_field($x);
+ }
+ break;
+ case 'exclude_categories':
+ case 'post_types':
+ case 'import_to_categories':
+ $safe_v = array();
+ foreach ($v as $x) {
+ $safe_v[] = sanitize_text_field($x);
+ }
+ $safe_input[$k] = $safe_v;
+ break;
+ case 'auto_source':
+ if ('Y' === $v || 'N' === $v) {
+ $safe_input[$k] = $v;
+ }
+ break;
+ case 'additional_markup':
+ $safe_input[$k] = trim($v);
+ break;
+ case 'use_excerpt':
+ case 'exclude_tags':
+ case 'auto_tweet':
+ case 'debug':
+ $safe_input[$k] = intval($v);
+ break;
+ case 'additional_tags':
+ if (is_string($v)) {
+ $tags = explode(',', $v);
+ $safe_tags = array();
+ foreach ($tags as $t) {
+ $safe_tags[] = sanitize_text_field($t);
+ }
+ $safe_input[$k] = $safe_tags;
+ }
+ break;
+ }
+ }
+ return $safe_input;
+ }
+
+ public function registerAdminMenu () {
+ add_options_page(
+ __('Tumblr Crosspostr Settings', 'tumblr-crosspostr'),
+ __('Tumblr Crosspostr', 'tumblr-crosspostr'),
+ 'manage_options',
+ $this->prefix . '_settings',
+ array($this, 'renderOptionsPage')
+ );
+
+ add_management_page(
+ __('Tumblrize Archives', 'tumblr-crosspostr'),
+ __('Tumblrize Archives', 'tumblr-crosspostr'),
+ 'manage_options',
+ $this->prefix . '_crosspost_archives',
+ array($this, 'dispatchTumblrizeArchivesPages')
+ );
+ }
+
+ public function registerAdminScripts () {
+ wp_register_style('tumblr-crosspostr', plugins_url('tumblr-crosspostr.css', __FILE__));
+ wp_enqueue_style('tumblr-crosspostr');
+ }
+
+ public function addMetaBox ($post) {
+ $options = get_option($this->prefix . '_settings');
+ if (empty($options['post_types'])) { $options['post_types'] = array(); }
+ $options['post_types'][] = 'post';
+ $options['post_types'] = apply_filters($this->prefix . '_meta_box_post_types', $options['post_types']);
+ foreach ($options['post_types'] as $cpt) {
+ add_meta_box(
+ 'tumblr-crosspostr-meta-box',
+ __('Tumblr Crosspostr', 'tumblr-crosspostr'),
+ array($this, 'renderMetaBox'),
+ $cpt,
+ 'side'
+ );
+ }
+ }
+
+ private function isConnectedToService () {
+ $options = get_option($this->prefix . '_settings');
+ return isset($this->tumblr) && get_option($this->prefix . '_access_token');
+ }
+
+ private function disconnectFromService () {
+ @$this->tumblr->client->ResetAccessToken(); // Suppress session_start() warning.
+ delete_option($this->prefix . '_access_token');
+ delete_option($this->prefix . '_access_token_secret');
+ }
+
+ public function renderMetaBox ($post) {
+ if (!$this->isConnectedToService()) {
+ $this->showError(__('Tumblr Crossposter does not yet have a connection to Tumblr. Are you sure you connected Tumblr Crosspostr to your Tumblr account?', 'tumblr-crosspostr'));
+ return;
+ }
+ $options = get_option($this->prefix . '_settings');
+
+ // Set default crossposting options for this post.
+ $x = get_post_meta($post->ID, $this->prefix . '_crosspost', true);
+ $d = $this->getTumblrBasename($post->ID);
+ $e = $this->getTumblrUseExcerpt($post->ID);
+ $s = get_post_meta($post->ID, 'tumblr_source_url', true);
+
+ $tumblr_id = get_post_meta($post->ID, 'tumblr_post_id', true);
+ if ('publish' === $post->post_status && $tumblr_id) {
+?>
+<p>
+ <a href="<?php print esc_attr($this->getSyndicatedAddress($post->ID));?>" class="button button-small"><?php esc_html_e('View post on Tumblr', 'tumblr-crosspostr');?></a>
+</p>
+<?php
+ }
+?>
+<fieldset>
+ <legend style="display:block;"><?php esc_html_e('Send this post to Tumblr?', 'tumblr-crosspostr');?></legend>
+ <p class="description" style="float: right; width: 75%;"><?php esc_html_e('If this post is in a category that Tumblr Crosspostr excludes, this will be ignored.', 'tumblr-crosspostr');?></p>
+ <ul>
+ <li><label><input type="radio" name="<?php esc_attr_e($this->prefix);?>_crosspost" value="Y"<?php if ('N' !== $x) { print ' checked="checked"'; }?>> <?php esc_html_e('Yes', 'tumblr-crosspostr');?></label></li>
+ <li><label><input type="radio" name="<?php esc_attr_e($this->prefix);?>_crosspost" value="N"<?php if ('N' === $x) { print ' checked="checked"'; }?>> <?php esc_html_e('No', 'tumblr-crosspostr');?></label></li>
+ </ul>
+</fieldset>
+<fieldset>
+ <legend><?php esc_html_e('Crossposting options', 'tumblr-crosspostr');?></legend>
+ <details open="open">
+ <summary><?php esc_html_e('Destination & content', 'tumblr-crosspostr');?></summary>
+ <p><label>
+ <?php esc_html_e('Send to my Tumblr blog titled', 'tumblr-crosspostr');?>
+ <?php print $this->tumblrBlogsSelectField(array('name' => $this->prefix . '_destination'), $d);?>
+ </label></p>
+ <p><label>
+ <input type="checkbox" name="<?php esc_attr_e($this->prefix);?>_use_excerpt" value="1"
+ <?php if (1 === $e) { print 'checked="checked"'; } ?>
+ title="<?php esc_html_e('Uncheck to send post content as crosspost content.', 'tumblr-crosspostr');?>"
+ />
+ <?php esc_html_e('Send excerpt instead of main content?', 'tumblr-crosspostr');?>
+ </label></p>
+ <p>
+ <label><?php esc_html_e('Content source:', 'tumblr-crosspostr');?>
+ <input id="<?php esc_attr_e($this->prefix);?>_meta_source_url" type="text"
+ name="<?php esc_attr_e($this->prefix);?>_meta_source_url"
+ title="<?php esc_attr_e('Set the content source of this post on Tumblr by pasting the URL where you found the content you are blogging about.', 'tumblr-crosspostr'); if ($options['auto_source'] === 'Y') { print ' ' . esc_attr__('Leave this blank to set the content source URL of your Tumblr post to the permalink of this WordPress post.', 'tumblr-crosspostr'); } ?>"
+ value="<?php esc_attr_e($s);?>"
+ placeholder="<?php esc_attr_e('//original-source.com/', 'tumblr-crosspostr');?>" />
+ <span class="description"><?php esc_html_e('Provide source attribution, if relevant.', 'tumblr-crosspostr');?></span>
+ </label>
+ </p>
+ </details>
+</fieldset>
+ <?php if ($post->post_status !== 'publish') { ?>
+<fieldset>
+ <legend><?php esc_html_e('Social media broadcasts', 'tumblr-crosspostr');?></legend>
+ <details open="open"><!-- Leave open until browsers work out their keyboard accessibility issues with this. -->
+ <summary><?php esc_html_e('Twitter', 'tumblr-crosspostr');?></summary>
+ <p>
+ <label>
+ <input type="checkbox" name="<?php esc_attr_e($this->prefix);?>_send_tweet" value="1"
+ <?php if (!empty($options['auto_tweet'])) { ?>checked="checked"<?php } ?>
+ title="<?php esc_html_e('Uncheck to disable the auto-tweet.', 'tumblr-crosspostr');?>"
+ />
+ <?php esc_html_e('Send tweet?', 'tumblr-crosspostr');?>
+ </label>
+ <label>
+ <input id="<?php esc_attr_e($this->prefix);?>_tweet_text" type="text"
+ name="<?php esc_attr_e($this->prefix);?>_tweet_text"
+ title="<?php esc_attr_e('If your Tumblr automatically tweets new posts to your Twitter account, you can customize the default tweet text by entering it here.', 'tumblr-crosspostr');?>"
+ placeholder="<?php print sprintf(esc_attr__('New post: %s :)', 'tumblr-crosspostr'), '[URL]');?>"
+ maxlength="140" />
+ <span class="description"><?php print sprintf(esc_html__('Use %s where you want the link to your Tumblr post to appear, or leave blank to use the default Tumblr auto-tweet.', 'tumblr-crosspostr'), '<code>[URL]</code>');?></span>
+ </label>
+ </p>
+ </details>
+</fieldset>
+<?php
+ }
+ }
+
+ /**
+ * Writes the HTML for the options page, and each setting, as needed.
+ */
+ // TODO: Add contextual help menu to this page.
+ public function renderOptionsPage () {
+ if (!current_user_can('manage_options')) {
+ wp_die(__('You do not have sufficient permissions to access this page.', 'tumblr-crosspostr'));
+ }
+ $options = get_option($this->prefix . '_settings');
+ if (empty($options['post_types'])) { $options['post_types'] = array(); }
+ $options['post_types'][] = 'post';
+ if (isset($_GET['disconnect']) && wp_verify_nonce($_GET[$this->prefix . '_nonce'], 'disconnect_from_tumblr')) {
+ $this->disconnectFromService();
+?>
+<div class="updated">
+ <p>
+ <?php esc_html_e('Disconnected from Tumblr.', 'tumblr-crosspostr');?>
+ <span class="description"><?php esc_html_e('The connection to Tumblr was disestablished. You can reconnect using the same credentials, or enter different credentials before reconnecting.', 'tumblr-crosspostr');?></span>
+ </p>
+</div>
+<?php
+ }
+?>
+<h2><?php esc_html_e('Tumblr Crosspostr Settings', 'tumblr-crosspostr');?></h2>
+<p class="fieldset-toc"><?php esc_html_e('Jump to options:', 'tumblr-crosspostr');?></p>
+<ul class="fieldset-toc">
+ <li><a href="#connection-to-tumblr"><?php esc_html_e('Connection to Tumblr', 'tumblr-crosspostr');?></a></li>
+ <li><a href="#crossposting-options"><?php esc_html_e('Crossposting options', 'tumblr-crosspostr');?></a></li>
+ <li><a href="#sync-options"><?php esc_html_e('Sync options', 'tumblr-crosspostr');?></a></li>
+ <li><a href="#plugin-extras"><?php esc_html_e('Plugin extras', 'tumblr-crosspostr');?></a></li>
+</ul>
+<form method="post" action="options.php">
+<?php settings_fields($this->prefix . '_settings');?>
+<fieldset id="connection-to-tumblr"><legend><?php esc_html_e('Connection to Tumblr', 'tumblr-crosspostr');?></legend>
+<table class="form-table" summary="<?php esc_attr_e('Required settings to connect to Tumblr.', 'tumblr-crosspostr');?>">
+ <tbody>
+ <tr<?php if (get_option($this->prefix . '_access_token')) : print ' style="display: none;"'; endif;?>>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_consumer_key"><?php esc_html_e('Tumblr API key/OAuth consumer key', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <input id="<?php esc_attr_e($this->prefix);?>_consumer_key" name="<?php esc_attr_e($this->prefix);?>_settings[consumer_key]" value="<?php empty($options['consumer_key']) ? print '' : esc_attr_e($options['consumer_key']);?>" placeholder="<?php esc_attr_e('Paste your API key here', 'tumblr-crosspostr');?>" />
+ <p class="description">
+ <?php esc_html_e('Your Tumblr API key is also called your consumer key.', 'tumblr-crosspostr');?>
+ <?php print sprintf(
+ esc_html__('If you need an API key, you can %s.', 'tumblr-crosspostr'),
+ '<a href="' . esc_attr($this->getTumblrAppRegistrationUrl()) . '" target="_blank" ' .
+ 'title="' . __('Get an API key from Tumblr by registering your WordPress blog as a new Tumblr app.', 'tumblr-crosspostr') . '">' .
+ __('create one here', 'tumblr-crosspostr') . '</a>'
+ );?>
+ </p>
+ </td>
+ </tr>
+ <tr<?php if (get_option($this->prefix . '_access_token')) : print ' style="display: none;"'; endif;?>>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_consumer_secret"><?php esc_html_e('OAuth consumer secret', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <input id="<?php esc_attr_e($this->prefix);?>_consumer_secret" name="<?php esc_attr_e($this->prefix);?>_settings[consumer_secret]" value="<?php empty($options['consumer_secret']) ? '' : esc_attr_e($options['consumer_secret']);?>" placeholder="<?php esc_attr_e('Paste your consumer secret here', 'tumblr-crosspostr');?>" />
+ <p class="description">
+ <?php esc_html_e('Your consumer secret is like your app password. Never share this with anyone.', 'tumblr-crosspostr');?>
+ </p>
+ </td>
+ </tr>
+ <?php if (!get_option($this->prefix . '_access_token') && isset($options['consumer_key']) && isset($options['consumer_secret'])) { ?>
+ <tr>
+ <th class="wp-ui-notification" style="border-radius: 5px; padding: 10px;">
+ <label for="<?php esc_attr_e($this->prefix);?>_oauth_authorize"><?php esc_html_e('Connect to Tumblr:', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <a href="<?php print wp_nonce_url(admin_url('options-general.php?page=' . $this->prefix . '_settings&' . $this->prefix . '_oauth_authorize'), 'tumblr-authorize');?>" class="button button-primary"><?php esc_html_e('Click here to connect to Tumblr', 'tumblr-crosspostr');?></a>
+ </td>
+ </tr>
+ <?php } else if (get_option($this->prefix . '_access_token')) { ?>
+ <tr>
+ <th colspan="2">
+ <div class="updated">
+ <p>
+ <?php esc_html_e('Connected to Tumblr!', 'tumblr-crosspostr');?>
+ <a href="<?php print wp_nonce_url(admin_url('options-general.php?page=' . $this->prefix . '_settings&disconnect'), 'disconnect_from_tumblr', $this->prefix . '_nonce');?>" class="button"><?php esc_html_e('Disconnect', 'tumblr-crosspostr');?></a>
+ <span class="description"><?php esc_html_e('Disconnecting will stop cross-posts from appearing on or being imported from your Tumblr blog(s), and will reset the options below to their defaults. You can re-connect at any time.', 'tumblr-crosspostr');?></span>
+ </p>
+ </div>
+ </th>
+ </tr>
+ <?php } ?>
+ </tbody>
+</table>
+</fieldset>
+ <?php if (get_option($this->prefix . '_access_token')) { ?>
+<fieldset id="crossposting-options"><legend><?php esc_html_e('Crossposting options', 'tumblr-crosspostr');?></legend>
+<table class="form-table" summary="<?php esc_attr_e('Options for customizing crossposting behavior.', 'tumblr-crosspostr');?>">
+ <tbody>
+ <tr<?php if (!isset($options['default_hostname'])) : print ' class="wp-ui-highlight"'; endif;?>>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_default_hostname"><?php esc_html_e('Default Tumblr blog for crossposts', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <?php print $this->tumblrBlogsSelectField(array('id' => $this->prefix . '_default_hostname', 'name' => $this->prefix . '_settings[default_hostname]'), $this->getTumblrBasename(0));?>
+ <p class="description"><?php esc_html_e('Choose which Tumblr blog you want to send your posts to by default. This can be overriden on a per-post basis, too.', 'tumblr-crosspostr');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_exclude_categories"><?php esc_html_e('Do not crosspost entries in these categories:', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <ul id="<?php esc_attr_e($this->prefix);?>_exclude_categories">
+ <?php foreach (get_categories(array('hide_empty' => 0)) as $cat) : ?>
+ <li>
+ <label>
+ <input
+ type="checkbox"
+ <?php if (isset($options['exclude_categories']) && in_array($cat->slug, $options['exclude_categories'])) : print 'checked="checked"'; endif;?>
+ value="<?php esc_attr_e($cat->slug);?>"
+ name="<?php esc_attr_e($this->prefix);?>_settings[exclude_categories][]">
+ <?php print esc_html($cat->name);?>
+ </label>
+ </li>
+ <?php endforeach;?>
+ </ul>
+ <p class="description"><?php esc_html_e('Will cause posts in the specificied categories never to be crossposted to Tumblr. This is useful if, for instance, you are creating posts automatically using another plugin and wish to avoid a feedback loop of crossposting back and forth from one service to another.', 'tumblr-crosspostr');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_auto_source_yes"><?php esc_html_e('Use permalinks from this blog as the "Content source" for crossposts on Tumblr?');?></label>
+ </th>
+ <td>
+ <ul style="float: left;">
+ <li>
+ <label>
+ <input type="radio" id="<?php esc_attr_e($this->prefix);?>_auto_source_yes"
+ name="<?php esc_attr_e($this->prefix);?>_settings[auto_source]"
+ <?php if (!isset($options['auto_source']) || $options['auto_source'] === 'Y') { print 'checked="checked"'; } ?>
+ value="Y" />
+ <?php esc_html_e('Yes', 'tumblr-crosspostr');?>
+ </label>
+ </li>
+ <li>
+ <label>
+ <input type="radio" id="<?php esc_attr_e($this->prefix);?>_auto_source_no"
+ name="<?php esc_attr_e($this->prefix);?>_settings[auto_source]"
+ <?php if (isset($options['auto_source']) && $options['auto_source'] === 'N') { print 'checked="checked"'; } ?>
+ value="N" />
+ <?php esc_html_e('No', 'tumblr-crosspostr');?>
+ </label>
+ </li>
+ </ul>
+ <p class="description" style="padding: 0 5em;"><?php print sprintf(esc_html__('When enabled, leaving the %sContent source%s field blank on a given entry will result in setting %sthe "Content source" field on your Tumblr post%s to the permalink of your WordPress post. Useful for providing automatic back-links to your main blog, but turn this off if you "secretly" use Tumblr Crosspostr as the back-end of a publishing platform.', 'tumblr-crosspostr'), '<code>', '</code>', '<a href="http://staff.tumblr.com/post/1059624418/content-attribution">', '</a>');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_use_excerpt"><?php esc_html_e('Send excerpts instead of main content?', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <input type="checkbox" <?php if (isset($options['use_excerpt'])) : print 'checked="checked"'; endif; ?> value="1" id="<?php esc_attr_e($this->prefix);?>_use_excerpt" name="<?php esc_attr_e($this->prefix);?>_settings[use_excerpt]" />
+ <label for="<?php esc_attr_e($this->prefix);?>_use_excerpt"><span class="description"><?php esc_html_e('When enabled, the excerpts (as opposed to the body) of your WordPress posts will be used as the main content of your Tumblr posts. Useful if you prefer to crosspost summaries instead of the full text of your entires to Tumblr by default. This can be overriden on a per-post basis, too.', 'tumblr-crosspostr');?></span></label>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_post_types"><?php esc_html_e('Crosspost the following post types:', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <ul id="<?php esc_attr_e($this->prefix);?>_post_types">
+ <?php foreach (get_post_types(array('public' => true)) as $cpt) : ?>
+ <li>
+ <label>
+ <input
+ type="checkbox"
+ <?php if (isset($options['post_types']) && in_array($cpt, $options['post_types'])) : print 'checked="checked"'; endif;?>
+ <?php if ('post' === $cpt) { print 'disabled="disabled"'; } ?>
+ value="<?php esc_attr_e($cpt);?>"
+ name="<?php esc_attr_e($this->prefix);?>_settings[post_types][]">
+ <?php print esc_html($cpt);?>
+ </label>
+ </li>
+ <?php endforeach;?>
+ </ul>
+ <p class="description"><?php print sprintf(esc_html__('Choose which %1$spost types%2$s you want to crosspost. Not all post types can be crossposted safely, but many can. If you are not sure about a post type, leave it disabled. Plugin authors may create post types that are crossposted regardless of the value of this setting. %3$spost%4$s post types are always enabled.', 'tumblr-crosspostr'), '<a href="https://codex.wordpress.org/Post_Types">', '</a>', '<code>', '</code>');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_additional_markup"><?php esc_html_e('Add the following markup to each crossposted entry:', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <textarea
+ id="<?php esc_attr_e($this->prefix);?>_additional_markup"
+ name="<?php esc_attr_e($this->prefix);?>_settings[additional_markup]"
+ placeholder="<?php esc_attr_e('Anything you type in this box will be added to every crosspost.', 'tumblr-crosspostr');?>"><?php
+ if (isset($options['additional_markup'])) {
+ print esc_textarea($options['additional_markup']);
+ } else {
+ print '<p class="tumblr-crosspostr-linkback"><a href="%permalink%" title="' . esc_html__('Go to the original post.', 'tumblr-crosspostr') . '" rel="bookmark">%the_title%</a> ' . esc_html__('was originally published on', $this->prefix . '') . ' <a href="%blog_url%">%blog_name%</a></p>';
+ }
+?></textarea>
+ <p class="description"><?php _e('Text or HTML you want to add to each post. Useful for things like a link back to your original post. You can use <code>%permalink%</code>, <code>%the_title%</code>, <code>%blog_url%</code>, and <code>%blog_name%</code> as placeholders for the cross-posted post\'s link, its title, the link to the homepage for this site, and the name of this blog, respectively. Leave this blank or use this field for a different purpose if you prefer to use only the Tumblr "Content source" meta field for links back to your main blog.', 'tumblr-crosspostr');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_exclude_tags"><?php esc_html_e('Do not send post tags to Tumblr', 'tumblr-crosspostr');?></label>
+ </th>
+ <td>
+ <input type="checkbox" <?php if (isset($options['exclude_tags'])) : print 'checked="checked"'; endif; ?> value="1" id="<?php esc_attr_e($this->prefix);?>_exclude_tags" name="<?php esc_attr_e($this->prefix);?>_settings[exclude_tags]" />
+ <label for="<?php esc_attr_e($this->prefix);?>_exclude_tags"><span class="description"><?php esc_html_e('When enabled, tags on your WordPress posts are not applied to your Tumblr posts. Useful if you maintain different taxonomies on your different sites.', 'tumblr-crosspostr');?></span></label>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_additional_tags">
+ <?php esc_html_e('Automatically add these tags to all crossposts:', 'tumblr-crosspostr');?>
+ </label>
+ </th>
+ <td>
+ <input id="<?php esc_attr_e($this->prefix);?>_additional_tags" value="<?php if (isset($options['additional_tags'])) : esc_attr_e(implode(', ', $options['additional_tags'])); endif;?>" name="<?php esc_attr_e($this->prefix);?>_settings[additional_tags]" placeholder="<?php esc_attr_e('crosspost, magic', 'tumblr-crosspostr');?>" />
+ <p class="description"><?php print sprintf(esc_html__('Comma-separated list of additional tags that will be added to every post sent to Tumblr. Useful if only some posts on your Tumblr blog are cross-posted and you want to know which of your Tumblr posts were generated by this plugin. (These tags will always be applied regardless of the value of the "%s" option.)', 'tumblr-crosspostr'), esc_html__('Do not send post tags to Tumblr', 'tumblr-crosspostr'));?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_auto_tweet">
+ <?php esc_html_e('Automatically tweet a link to your Tumblr post?', 'tumblr-crosspostr');?>
+ </label>
+ </th>
+ <td>
+ <input type="checkbox" <?php if (isset($options['auto_tweet'])) : print 'checked="checked"'; endif; ?> value="1" id="<?php esc_attr_e($this->prefix);?>_auto_tweet" name="<?php esc_attr_e($this->prefix);?>_settings[auto_tweet]" />
+ <label for="<?php esc_attr_e($this->prefix);?>_auto_tweet"><span class="description"><?php print sprintf(esc_html__('When checked, new posts you create on WordPress will have their "%s" option enabled by default. You can always override this when editing an individual post.', 'tumblr-crosspostr'), esc_html__('Send tweet?', 'tumblr-crosspostr'));?></span></label>
+ </td>
+ </tr>
+ </tbody>
+</table>
+</fieldset>
+<fieldset id="sync-options"><legend><?php esc_html_e('Sync options', 'tumblr-crosspostr');?></legend>
+<table class="form-table" summary="<?php esc_attr_e('Customize the import behavior.', 'tumblr-crosspostr');?>">
+ <tbody>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_sync_from_tumblr"><?php esc_html_e('Sync posts from Tumblr', 'tumblr-crosspostr');?></label>
+ <p class="description"><?php esc_html_e('(This feature is experimental. Please backup your website before you turn this on.)', 'tumblr-crosspostr');?></p>
+ </th>
+ <td>
+ <ul id="<?php esc_attr_e($this->prefix);?>_sync_content">
+ <?php print $this->tumblrBlogsListCheckboxes(
+ array(
+ 'id' => $this->prefix . '_sync_content',
+ 'name' => $this->prefix . '_settings[sync_content][]'
+ ),
+ (empty($options['sync_content'])) ? false : $options['sync_content']
+ );?>
+ </ul>
+ <p class="description"><?php esc_html_e('Content you create on the Tumblr blogs you select will automatically be copied to this blog.', 'tumblr-crosspostr');?></p>
+ </td>
+ </tr>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_import_to_categories"><?php esc_html_e('Automatically assign synced posts to these categories:');?></label>
+ </th>
+ <td>
+ <ul id="<?php esc_attr_e($this->prefix);?>_import_to_categories">
+ <?php foreach (get_categories(array('hide_empty' => 0)) as $cat) : ?>
+ <li>
+ <label>
+ <input
+ type="checkbox"
+ <?php if (isset($options['import_to_categories']) && in_array($cat->slug, $options['import_to_categories'])) : print 'checked="checked"'; endif;?>
+ value="<?php esc_attr_e($cat->slug);?>"
+ name="<?php esc_attr_e($this->prefix);?>_settings[import_to_categories][]">
+ <?php print esc_html($cat->name);?>
+ </label>
+ </li>
+ <?php endforeach;?>
+ </ul>
+ <p class="description"><?php print sprintf(esc_html__('Will cause any posts imported from your Tumblr blog to be assigned the categories that you enable here. It is often a good idea to %screate a new category%s that you use exclusively for this purpose.', 'tumblr-crosspostr'), '<a href="' . admin_url('edit-tags.php?taxonomy=category') . '">', '</a>');?></p>
+ </td>
+ </tr>
+ </tbody>
+</table>
+</fieldset>
+<fieldset id="plugin-extras"><legend><?php esc_html_e('Plugin extras', 'tumblr-crosspostr');?></legend>
+<table class="form-table" summary="<?php esc_attr_e('Additional options to customize plugin behavior.', 'tumblr-crosspostr');?>">
+ <tbody>
+ <tr>
+ <th>
+ <label for="<?php esc_attr_e($this->prefix);?>_debug">
+ <?php esc_html_e('Enable detailed debugging information?', 'tumblr-crosspostr');?>
+ </label>
+ </th>
+ <td>
+ <input type="checkbox" <?php if (isset($options['debug'])) : print 'checked="checked"'; endif; ?> value="1" id="<?php esc_attr_e($this->prefix);?>_debug" name="<?php esc_attr_e($this->prefix);?>_settings[debug]" />
+ <label for="<?php esc_attr_e($this->prefix);?>_debug"><span class="description"><?php
+ print sprintf(
+ esc_html__('Turn this on only if you are experiencing problems using this plugin, or if you were told to do so by someone helping you fix a problem (or if you really know what you are doing). When enabled, extremely detailed technical information is displayed as a WordPress admin notice when you take actions like sending a crosspost. If you have also enabled WordPress\'s built-in debugging (%1$s) and debug log (%2$s) feature, additional information will be sent to a log file (%3$s). This file may contain sensitive information, so turn this off and erase the debug log file when you have resolved the issue.', 'tumblr-crosspostr'),
+ '<a href="https://codex.wordpress.org/Debugging_in_WordPress#WP_DEBUG"><code>WP_DEBUG</code></a>',
+ '<a href="https://codex.wordpress.org/Debugging_in_WordPress#WP_DEBUG_LOG"><code>WP_DEBUG_LOG</code></a>',
+ '<code>' . content_url() . '/debug.log' . '</code>'
+ );
+ ?></span></label>
+ </td>
+ </tr>
+ </tbody>
+</table>
+</fieldset>
+ <?php } ?>
+<?php submit_button();?>
+</form>
+<?php
+ $this->showDonationAppeal();
+ } // end public function renderOptionsPage
+
+ public function dispatchTumblrizeArchivesPages () {
+ if (!isset($_GET[$this->prefix . '_nonce']) || !wp_verify_nonce($_GET[$this->prefix . '_nonce'], 'tumblrize_everything')) {
+ $this->renderManagementPage();
+ } else {
+ if (!$this->isConnectedToService()) {
+ wp_redirect(admin_url('options-general.php?page=' . $this->prefix . '_settings'));
+ exit();
+ }
+ $posts = get_posts(array(
+ 'nopaging' => true,
+ 'order' => 'ASC',
+ ));
+ $tumblrized = array();
+ foreach ($posts as $post) {
+ if ($prepared_post = $this->prepareForTumblr($post->ID)) {
+ $data = $this->crosspostToTumblr($prepared_post->base_hostname, $prepared_post->params, $prepared_post->tumblr_id);
+ update_post_meta($post->ID, 'tumblr_post_id', $data->response->id);
+ $tumblrized[] = array('id' => $data->response->id, 'base_hostname' => $prepared_post->base_hostname);
+ }
+ }
+ $blogs = array();
+ foreach ($tumblrized as $p) {
+ $blogs[] = $p['base_hostname'];
+ }
+ $blogs_touched = count(array_unique($blogs));
+ $posts_touched = count($tumblrized);
+ print '<p>' . sprintf(
+ _n(
+ 'Success! %1$d post has been crossposted.',
+ 'Success! %1$d posts have been crossposted to %2$d blogs.',
+ $posts_touched,
+ 'tumblr-crosspostr'
+ ),
+ $posts_touched,
+ $blogs_touched
+ ) . '</p>';
+ print '<p>' . esc_html_e('Blogs touched:', 'tumblr-crosspostr') . '</p>';
+ print '<ul>';
+ foreach (array_unique($blogs) as $blog) {
+ print '<li><a href="' . esc_url("http://$blog/") . '">' . esc_html($blog) . '</a></li>';
+ }
+ print '</ul>';
+ $this->showDonationAppeal();
+ }
+ }
+
+ private function renderManagementPage () {
+ $options = get_option($this->prefix . '_settings');
+?>
+<h2><?php esc_html_e('Crosspost Archives to Tumblr', 'tumblr-crosspostr');?></h2>
+<p><?php esc_html_e('If you have post archives on this website, Tumblr Crosspostr can copy them to your Tumblr blog.', 'tumblr-crosspostr');?></p>
+<p><a href="<?php print wp_nonce_url(admin_url('tools.php?page=' . $this->prefix . '_crosspost_archives'), 'tumblrize_everything', $this->prefix . '_nonce');?>" class="button button-primary">Tumblrize Everything!</a></p>
+<p class="description"><?php print sprintf(esc_html__('Copies all posts from your archives to your default Tumblr blog (%s). This may take some time if you have a lot of content. If you do not want to crosspost a specific post, set the answer to the "Send this post to Tumblr?" question to "No" when editing those posts before taking this action. If you have previously crossposted some posts, this will update that content on your Tumblr blog(s).', 'tumblr-crosspostr'), '<code>' . esc_html($options['default_hostname']) . '</code>');?></p>
+<?php
+ $this->showDonationAppeal();
+ } // end renderManagementPage ()
+
+ private function getBlogsToSync () {
+ $options = get_option($this->prefix . '_settings');
+ return (empty($options['sync_content'])) ? array() : $options['sync_content'];
+ }
+
+ public function setSyncSchedules () {
+ if (!$this->isConnectedToService()) { return; }
+ $blogs_to_sync = $this->getBlogsToSync();
+ // If we are being asked to sync, set up a daily schedule for that.
+ if (!empty($blogs_to_sync)) {
+ foreach ($blogs_to_sync as $x) {
+ if (!wp_get_schedule($this->prefix . '_sync_content', array($x))) {
+ wp_schedule_event(time(), 'daily', $this->prefix . '_sync_content', array($x));
+ }
+ }
+ }
+ // For any blogs we know of but aren't being asked to sync,
+ $known_blogs = array();
+ $users_blogs = $this->tumblr->getUserBlogs();
+ if ($users_blogs) {
+ foreach ($users_blogs as $blog) {
+ $known_blogs[] = parse_url($blog->url, PHP_URL_HOST);
+ }
+ }
+ $to_unschedule = array_diff($known_blogs, $blogs_to_sync);
+ foreach ($to_unschedule as $x) {
+ // check to see if there's a scheduled event to sync it, and,
+ // if so, unschedule it.
+ wp_unschedule_event(
+ wp_next_scheduled($this->prefix . '_sync_content', array($x)),
+ $this->prefix . '_sync_content',
+ array($x)
+ );
+ }
+ }
+
+ public function deactivate () {
+ $blogs_to_sync = $this->getBlogsToSync();
+ if (!empty($blogs_to_sync)) {
+ foreach ($blogs_to_sync as $blog) {
+ wp_clear_scheduled_hook($this->prefix . '_sync_content', array($blog));
+ }
+ }
+ }
+
+ /**
+ * There's a frustrating bug in PHP? In WordPress? That causes get_posts()
+ * to always return an empty array when run in Cron if the PHP version is
+ * less than 5.4-ish. Check for that and workaround if necessary.
+ *
+ * @see https://wordpress.org/support/topic/get_posts-returns-no-results-when-run-via-cron
+ */
+ private function crosspostExists ($tumblr_id) {
+ // If we're running on PHP >= 5.4, use WordPress's built-in function.
+ if (version_compare(PHP_VERSION, '5.4.0') >= 0) {
+ return get_posts(array(
+ 'meta_key' => 'tumblr_post_id',
+ 'meta_value' => $tumblr_id,
+ 'fields' => 'ids'
+ ));
+ }
+ global $wpdb;
+ return $wpdb->get_col($wpdb->prepare(
+ "
+ SELECT post_id FROM {$wpdb->postmeta}
+ WHERE meta_key='%s' AND meta_value='%s'
+ ",
+ 'tumblr_post_id',
+ $tumblr_id
+ ));
+ }
+
+ public function syncFromTumblrBlog ($base_hostname) {
+ $options = get_option($this->prefix . '_settings');
+ if (!empty($options['debug'])) {
+ error_log(sprintf(esc_html__('Entering Tumblr Sync routine for %s', 'tumblr-crosspostr'), $base_hostname));
+ }
+ if (!isset($options['last_synced_ids'])) {
+ $options['last_synced_ids'] = array();
+ }
+ $latest_synced_id = (isset($options['last_synced_ids'][$base_hostname]))
+ ? $options['last_synced_ids'][$base_hostname]
+ : 0;
+
+ $this->tumblr->setApiKey($options['consumer_key']);
+
+ $ids_synced = array(0); // Init with 0
+ $offset = 0;
+ $limit = 50;
+ // This loop either:
+ // * Trawls through the entire blog (if $latest_synced_id is 0), or
+ // * only tries to sync the latest two batches of posts
+ do {
+ $resp = $this->tumblr->getPosts($base_hostname, array('offset' => $offset, 'limit' => $limit));
+ $posts = $resp->posts;
+ foreach ($posts as $post) {
+ $preexisting_posts = $this->crosspostExists($post->id);
+ if (!empty($options['debug'])) {
+ error_log(sprintf(
+ _n('Found %s preexisting post for Tumblr ID', 'Found %s preexisting posts for Tumblr ID', count($preexisting_posts), 'tumblr-crosspostr') . ' %s (%s)',
+ count($preexisting_posts),
+ $post->id,
+ implode(',', $preexisting_posts)
+ ));
+ }
+ if (in_array($post->id, $ids_synced)) {
+ error_log("Haven't we already sync'ed this Tumblr post? {$post->id}");
+ } else if (empty($preexisting_posts)) {
+ if ($this->importPostFromTumblr($post)) {
+ $ids_synced[] = $post->id;
+ }
+ }
+ }
+ $offset = ($limit + $offset);
+ if (0 !== $latest_synced_id && $offset >= $limit * 2) {
+ if (!empty($options['debug'])) {
+ error_log(esc_html__('Previously synced, stopping.', 'tumblr-crosspostr'));
+ }
+ break;
+ }
+ } while (!empty($posts));
+
+ // Record the latest Tumblr post ID to be sync'ed on the blog.
+ // (Usefully, Tumblr post ID's are sequential.)
+ $options['last_synced_ids'][$base_hostname] = ($latest_synced_id > max($ids_synced))
+ ? $latest_synced_id
+ : max($ids_synced);
+ update_option($this->prefix . '_settings', $options);
+ }
+
+ private function translateTumblrPostContent ($post) {
+ $content = '';
+ switch ($post->type) {
+ case 'photo':
+ foreach ($post->photos as $photo) {
+ $content .= '<img src="' . $photo->original_size->url . '" alt="' . $photo->caption. '" />';
+ }
+ $content .= $post->caption;
+ break;
+ case 'quote':
+ $content .= '<blockquote>' . $post->text . '</blockquote>';
+ $content .= $post->source;
+ break;
+ case 'link':
+ $content .= '<a href="' . $post->url . '">' . $post->title . '</a>';
+ $content .= $post->description;
+ break;
+ case 'audio':
+ $content .= $post->player;
+ $content .= $post->caption;
+ break;
+ case 'video':
+ $content .= $post->player[0]->embed_code;
+ $content .= $post->caption;
+ break;
+ case 'answer':
+ $content .= '<a href="' . $post->asking_url .'" class="tumblr_blog">' . $post->asking_name . '</a>:';
+ $content .= '<blockquote cite="' . $post->post_url . '" class="tumblr_ask">' . $post->question . '</blockquote>';
+ $content .= $post->answer;
+ break;
+ case 'chat':
+ case 'text':
+ default:
+ $content .= $post->body;
+ break;
+ }
+ return $content;
+ }
+
+ private function importPostFromTumblr ($post) {
+ $options = get_option($this->prefix . '_settings');
+ if (!empty($options['debug'])) {
+ error_log(sprintf('Entering importPostFromTumblr, tumblr_post_id=%s', $post->id));
+ }
+ $wp_post = array();
+ $wp_post['post_content'] = $this->translateTumblrPostContent($post);
+ $wp_post['post_title'] = (isset($post->title)) ? $post->title : '';
+ $wp_post['post_status'] = $this->TumblrState2WordPressStatus($post->state);
+ // TODO: Figure out how to handle multi-author blogs.
+ //$wp_post['post_author'] = $post->author;
+ $wp_post['post_date'] = date('Y-m-d H:i:s', $post->timestamp);
+ $wp_post['post_date_gmt'] = gmdate('Y-m-d H:i:s', $post->timestamp);
+ $wp_post['tags_input'] = $post->tags;
+
+ if (!empty($options['import_to_categories'])) {
+ $cat_ids = array();
+ foreach ($options['import_to_categories'] as $slug) {
+ $x = get_category_by_slug($slug);
+ $cat_ids[] = $x->term_id;
+ }
+ $wp_post['post_category'] = $cat_ids;
+ }
+
+ // Remove filtering so we retain audio, video, embeds, etc.
+ remove_filter('content_save_pre', 'wp_filter_post_kses');
+ remove_action('save_post', array($this, 'savePost')); // avoid loops during import
+ $wp_id = wp_insert_post($wp_post);
+ add_filter('content_save_pre', 'wp_filter_post_kses');
+ if ($wp_id) {
+ set_post_format($wp_id, $this->TumblrPostType2WordPressPostFormat($post->type));
+ update_post_meta($wp_id, 'tumblr_base_hostname', parse_url($post->post_url, PHP_URL_HOST));
+ update_post_meta($wp_id, 'tumblr_post_id', $post->id);
+ update_post_meta($wp_id, 'tumblr_reblog_key', $post->reblog_key);
+ if (isset($post->source_url)) {
+ update_post_meta($wp_id, 'tumblr_source_url', $post->source_url);
+ }
+
+ // Import media from post types as WordPress attachments.
+ if (!empty($post->type)) {
+ $wp_subdir_from_post_timestamp = date('Y/m', $post->timestamp);
+ $wp_upload_dir = wp_upload_dir($wp_subdir_from_post_timestamp);
+ if (!is_writable($wp_upload_dir['path'])) {
+ $msg = sprintf(
+ esc_html__('Your WordPress uploads directory (%s) is not writeable, so Tumblr Crosspostr could not import some media files directly into your Media Library. Media (such as images) will be referenced from their remote source rather than imported and referenced locally.', 'tumblr-crosspostr'),
+ $wp_upload_dir['path']
+ );
+ error_log($msg);
+ } else {
+ switch ($post->type) {
+ case 'photo':
+ $this->importPhotosInPost($post, $wp_id);
+ break;
+ case 'audio':
+ $this->importAudioInPost($post, $wp_id);
+ break;
+ default:
+ // TODO: Import media from other post types?
+ break;
+ }
+ }
+ }
+ }
+ return $wp_id;
+ }
+
+ /**
+ * Imports some URL-accessible asset (image, audio file, etc.) as a
+ * WordPress attachment and associates it with a given WordPress post.
+ *
+ * @param string $media_url The URL to the asset.
+ * @param object $post Tumblr post object.
+ * @param int $wp_id WordPress post ID number.
+ * @param string $replace_this Content referencing remote asset to replace with locally imported asset.
+ * @param string $replace_with_this_before Content to prepend to locally imported asset URL, such as the start of a shortcode.
+ * @param string $replace_with_this_after Content to append to locally imported asset URL, such as the end of a shortcode.
+ */
+ private function importMedia ($media_url, $post, $wp_id, $replace_this, $replace_with_this_before = '', $replace_with_this_after = '') {
+ $data = wp_remote_get($media_url, array('timeout' => 300)); // Download for 5 minutes, tops.
+ if (is_wp_error($data)) {
+ $msg = sprintf(
+ esc_html__('Failed to get Tumblr media (%1$s) from post (%2$s). Server responded: %3$s', 'tumblr-crosspostr'),
+ $media_url,
+ $post->post_url,
+ print_r($data, true)
+ );
+ error_log($msg);
+ } else {
+ $f = wp_upload_bits(basename($media_url), null, $data['body'], date('Y/m', $post->timestamp));
+ if ($f['error']) {
+ $msg = sprintf(
+ esc_html__('Error saving file (%s): ', 'tumblr-crosspostr'),
+ basename($media_url)
+ );
+ error_log($msg);
+ } else {
+ $wp_upload_dir = wp_upload_dir(date('Y/m', $post->timestamp));
+ $wp_filetype = wp_check_filetype(basename($f['file']));
+ $wp_file_id = wp_insert_attachment(array(
+ 'post_title' => basename($f['file'], ".{$wp_filetype['ext']}"),
+ 'post_content' => '', // Always empty string.
+ 'post_status' => 'inherit',
+ 'post_mime_type' => $wp_filetype['type'],
+ 'guid' => $wp_upload_dir['url'] . '/' . basename($f['file'])
+ ), $f['file'], $wp_id);
+ require_once(ABSPATH . 'wp-admin/includes/media.php');
+ require_once(ABSPATH . 'wp-admin/includes/image.php');
+ $metadata = wp_generate_attachment_metadata($wp_file_id, $f['file']);
+ wp_update_attachment_metadata($wp_file_id, $metadata);
+ if (1 === count($post->photos)) {
+ set_post_thumbnail($wp_id, $wp_file_id);
+ $photo = array_shift($post->photos);
+ $replace_this = '<img src="' . $photo->original_size->url . '" alt="' . $photo->caption. '" />';
+ $new_content = str_replace($replace_this, '', get_post_field('post_content', $wp_id));
+ } else {
+ $new_content = str_replace($replace_this, $replace_with_this_before . $f['url'] . $replace_with_this_after, get_post_field('post_content', $wp_id));
+ }
+ wp_update_post(array(
+ 'ID' => $wp_id,
+ 'post_content' => $new_content
+ ));
+ }
+ }
+ }
+
+ private function importPhotosInPost ($post, $wp_id) {
+ foreach ($post->photos as $photo) {
+ $this->importMedia($photo->original_size->url, $post, $wp_id, $photo->original_size->url);
+ }
+ }
+
+ private function importAudioInPost ($post, $wp_id) {
+ $player_atts = wp_kses_hair($post->player, array('http', 'https'));
+ $x = parse_url($player_atts['src']['value'], PHP_URL_QUERY);
+ $vars = array();
+ parse_str($x, $vars);
+ $audio_file_url = urldecode($vars['audio_file']);
+
+ $this->importMedia($audio_file_url, $post, $wp_id, $post->player, '[audio src="', '"]');
+ }
+
+ private function getTumblrAppRegistrationUrl () {
+ $params = array(
+ 'title' => get_bloginfo('name'),
+ // Max 400 chars for Tumblr
+ 'description' => mb_substr(get_bloginfo('description'), 0, 400, get_bloginfo('charset')),
+ 'url' => home_url(),
+ 'admin_contact_email' => get_bloginfo('admin_email'),
+ 'default_callback_url' => plugins_url('/oauth-callback.php', __FILE__)
+ );
+ return $this->tumblr->getAppRegistrationUrl($params);
+ }
+
+ private function tumblrBlogsSelectField ($attributes = array(), $selected = false) {
+ $html = '<select';
+ if (!empty($attributes)) {
+ foreach ($attributes as $k => $v) {
+ $html .= ' ' . $k . '="' . esc_attr($v) . '"';
+ }
+ }
+ $html .= '>';
+ foreach ($this->tumblr->getUserBlogs() as $blog) {
+ $html .= '<option value="' . esc_attr(parse_url($blog->url, PHP_URL_HOST)) . '"';
+ if ($selected && $selected === parse_url($blog->url, PHP_URL_HOST)) {
+ $html .= ' selected="selected"';
+ }
+ $html .= '>';
+ $html .= esc_html($blog->title);
+ $html .= '</option>';
+ }
+ $html .= '</select>';
+ return $html;
+ }
+
+ private function tumblrBlogsListCheckboxes ($attributes = array(), $selected = false) {
+ $html = '';
+ foreach ($this->tumblr->getUserBlogs() as $blog) {
+ $html .= '<li>';
+ $html .= '<label>';
+ $x = parse_url($blog->url, PHP_URL_HOST);
+ $html .= '<input type="checkbox"';
+ if (!empty($attributes)) {
+ foreach ($attributes as $k => $v) {
+ $html .= ' ';
+ switch ($k) {
+ case 'id':
+ $html .= $k . '="' . esc_attr($v) . '-' . esc_attr($x) . '"';
+ break;
+ default:
+ $html .= $k . '="' . esc_attr($v) . '"';
+ break;
+ }
+ }
+ }
+ if ($selected && in_array($x, $selected)) {
+ $html .= ' checked="checked"';
+ }
+ $html .= ' value="' . esc_attr($x) . '"';
+ $html .= '>';
+ $html .= esc_html($blog->title) . '</label>';
+ $html .= '</li>';
+ }
+ return $html;
+ }
+
+ // Modified from https://stackoverflow.com/a/4997018/2736587 which claims
+ // http://www.php.net/manual/en/function.strip-tags.php#96483
+ // as its source. Werksferme.
+ private function strip_only($str, $tags, $stripContent = false, $limit = -1) {
+ $content = '';
+ if(!is_array($tags)) {
+ $tags = (strpos($str, '>') !== false ? explode('>', str_replace('<', '', $tags)) : array($tags));
+ if(end($tags) == '') array_pop($tags);
+ }
+ foreach($tags as $tag) {
+ if ($stripContent) {
+ $content = '(.+</'.$tag.'[^>]*>|)';
+ }
+ $str = preg_replace('#</?'.$tag.'[^>]*>'.$content.'#is', '', $str, $limit);
+ }
+ return $str;
+ }
+
+}
+
+$tumblr_crosspostr = new Tumblr_Crosspostr();
+require_once 'template-functions.php';
--- /dev/null
+<?php\r
+/**\r
+ * Tumblr Crosspostr uninstaller\r
+ *\r
+ * @package plugin\r
+ */\r
+\r
+// Don't execute any uninstall code unless WordPress core requests it.\r
+if (!defined('WP_UNINSTALL_PLUGIN')) { exit(); }\r
+\r
+// Delete options.\r
+delete_option('tumblr_crosspostr_settings');\r
+delete_option('_tumblr_crosspostr_admin_notices');\r
+delete_option('tumblr_crosspostr_access_token');\r
+delete_option('tumblr_crosspostr_access_token_secret');\r
+\r
+delete_post_meta_by_key('tumblr_crosspostr_crosspost');\r
+/**\r
+ * TODO: Should we really delete this post meta?\r
+ * That'll wipe Tumblr post IDs and blog hostnames. :\\r
+ * We need these to be able to re-associate WordPress posts\r
+ * with the Tumblr posts that they were cross-posted to.\r
+ */\r
+// delete_post_meta_by_key('tumblr_post_id');\r
+// delete_post_meta_by_key('tumblr_base_hostname');\r