Merge branch 'refs/heads/dejal' into catalyst

This commit is contained in:
David Sinclair 2020-06-08 19:08:31 -07:00
commit 9466efe171
2444 changed files with 2101 additions and 86192 deletions

View file

@ -137,7 +137,6 @@ these after the installation below.
the `fabfile.py`. You should also have MySQL/PostgreSQL and MongoDB already installed.
fab -R local setup_python
fab -R local setup_imaging
fab -R local setup_mongoengine
fab -R local setup_forked_mongoengine
fab -R local setup_repo_local_settings

View file

@ -20,6 +20,7 @@ class LoginForm(forms.Form):
widget=forms.PasswordInput(attrs={'tabindex': 2, 'class': 'NB-input'}),
required=False)
# error_messages={'required': 'Please enter a password.'})
add = forms.CharField(required=False, widget=forms.HiddenInput())
def __init__(self, *args, **kwargs):
self.user_cache = None

View file

@ -171,6 +171,9 @@ def login(request):
code = 1
else:
logging.user(form.get_user(), "~FG~BBLogin~FW")
next_url = request.POST.get('next', '')
if next_url:
return HttpResponseRedirect(next_url)
return HttpResponseRedirect(reverse('index'))
else:
message = form.errors.items()[0][1][0]

View file

@ -417,7 +417,7 @@ class Feed(models.Model):
if url and url.startswith('newsletter:'):
return cls.objects.get(feed_address=url)
if url and re.match('(https?://)?twitter.com/\w+/?$', url):
if url and re.match('(https?://)?twitter.com/\w+/?', url):
without_rss = True
if url and re.match(r'(https?://)?(www\.)?facebook.com/\w+/?$', url):
without_rss = True

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.newsblur"
android:versionCode="163"
android:versionName="9.0.1" >
android:versionCode="165"
android:versionName="10.0b2" >
<uses-sdk
android:minSdkVersion="21"
@ -74,12 +74,18 @@
<activity
android:name=".activity.Settings"
android:label="@string/settings"/>
<activity
android:name=".activity.WidgetConfig"
android:launchMode="singleTask"
android:label="@string/widget" />
<activity
android:name=".activity.FeedItemsList" />
<activity
android:name=".activity.AllStoriesItemsList" />
android:name=".activity.AllStoriesItemsList"
android:launchMode="singleTask"/>
<activity
android:name=".activity.InfrequentItemsList" />
@ -137,17 +143,11 @@
<activity
android:name=".activity.SocialFeedReading"/>
<activity android:name=".widget.ConfigureWidgetActivity">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<service
android:name=".service.NBSyncService"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service android:name=".widget.BlurWidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"
/>
<service android:name=".widget.WidgetRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver android:name=".service.BootReceiver">
<intent-filter>
@ -158,13 +158,24 @@
<receiver android:name=".util.NotifyDismissReceiver" android:exported="false" />
<receiver android:name=".util.NotifySaveReceiver" android:exported="false" />
<receiver android:name=".util.NotifyMarkreadReceiver" android:exported="false" />
<receiver android:name=".widget.NewsBlurWidgetProvider" >
<receiver android:name=".widget.WidgetProvider"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/newsblur_appwidget_info" />
</receiver>
<receiver android:name=".service.TimeChangeReceiver">
<intent-filter >
<action android:name="android.intent.action.TIME_SET"/>
</intent-filter>
</receiver>
<receiver
android:name=".widget.WidgetUpdateReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.newsblur.fileprovider"

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ExpandableListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:groupIndicator="@null" />
<TextView
android:id="@+id/text_no_subscriptions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:padding="32dp"
android:text="@string/title_no_subscriptions"
android:visibility="gone" />
</FrameLayout>

View file

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:background="@color/white"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/txt_feed_name"
style="@style/rowItemHeaderBackground"
tools:text="Coding Horror"
android:paddingTop="9dp"
android:paddingBottom="9dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:shadowDy="1"
android:textStyle="bold"
android:lines="1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ListView
tools:listitem="@layout/newsblur_widget_item"
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<!-- Note that empty views must be siblings of the collection view
for which the empty view represents empty state.-->
</FrameLayout>

View file

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/newsblur_widget_item"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/widget_item_title"
android:paddingBottom="7dp"
android:paddingTop="7dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:ellipsize="end"
tools:text="The Next CEO of Stackoverflow"
android:lines="1"
android:textSize="14sp"
android:textStyle="bold"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="wrap_content"/>
<TextView
android:paddingEnd="8dp"
android:id="@+id/widget_item_time"
tools:text="30 min ago"
android:textSize="12sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp">
<CheckBox
android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false" />
<ImageView
android:id="@+id/img"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:contentDescription="@string/description_row_feed_icon"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/text_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1" />
<TextView
android:id="@+id/text_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container_folder"
style="?selectorFolderBackground"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp">
<ImageView
android:id="@+id/img_folder"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="2dp"
android:contentDescription="@string/description_row_folder_icon"
android:src="@drawable/g_icn_folder" />
<TextView
android:id="@+id/text_folder_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="32dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingTop="9dp"
android:paddingBottom="9dp"
android:textStyle="bold" />
</FrameLayout>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp" />

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widget_background"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:contentDescription="@string/description_login_logo"
android:scaleType="centerInside"
android:src="@drawable/logo" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="32dp"
android:lines="1"
android:text="@string/newsblur"
android:textAllCaps="true"
android:textColor="@color/widget_title"
android:textStyle="bold" />
</FrameLayout>
<ListView
android:id="@+id/widget_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/view_widget_story_item"
android:divider="@color/widget_divider"
android:dividerHeight="0.5dp"/>
<TextView
android:id="@+id/widget_empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/title_widget_setup"
android:textColor="@color/widget_feed_title" />
<!-- Note that empty views must be siblings of the collection view
for which the empty view represents empty state.-->
</LinearLayout>

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="@+id/view_widget_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingRight="8dp">
<ImageView
android:id="@+id/story_item_favicon_borderbar_1"
android:layout_width="5dp"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/story_item_favicon_borderbar_2"
android:layout_width="5dp"
android:layout_height="match_parent"
android:layout_toRightOf="@id/story_item_favicon_borderbar_1" />
<ImageView
android:id="@+id/story_item_feedicon"
android:layout_width="19dp"
android:layout_height="19dp"
android:layout_alignParentLeft="true"
android:layout_marginStart="18dp"
android:layout_marginTop="2dp"
android:layout_toRightOf="@+id/story_item_favicon_borderbar_2" />
<TextView
android:id="@+id/story_item_feedtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="2dp"
android:layout_toRightOf="@id/story_item_feedicon"
android:ellipsize="end"
android:singleLine="true"
android:textColor="@color/widget_feed_title"
android:textStyle="bold" />
<TextView
android:id="@+id/story_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/story_item_feedicon"
android:layout_marginLeft="8dp"
android:layout_marginTop="2dp"
android:layout_marginRight="8dp"
android:layout_toLeftOf="@+id/story_item_thumbnail"
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
android:ellipsize="end"
android:lines="2"
android:maxLines="2"
android:textColor="@color/widget_story_title"
android:textStyle="bold" />
<TextView
android:id="@+id/story_item_content"
style="?storySnippetText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/story_item_title"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_toLeftOf="@+id/story_item_thumbnail"
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
android:ellipsize="end"
android:lines="2"
android:maxLines="2"
android:textColor="@color/widget_story_content"
android:textSize="13sp" />
<ImageView
android:id="@+id/story_item_thumbnail"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_below="@+id/story_item_feedtitle"
android:layout_alignParentRight="true"
android:layout_marginTop="2dp"
android:scaleType="centerCrop"
android:visibility="gone" />
<TextView
android:id="@+id/story_item_author"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/story_item_content"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="2dp"
android:layout_toLeftOf="@id/story_item_date"
android:layout_toRightOf="@id/story_item_favicon_borderbar_2"
android:ellipsize="end"
android:maxLines="1"
android:textAllCaps="true"
android:textColor="@color/widget_story_author"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/story_item_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/story_item_content"
android:layout_alignParentRight="true"
android:layout_marginBottom="2dp"
android:gravity="end"
android:maxLines="1"
android:textColor="@color/widget_story_date_time"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

View file

@ -21,6 +21,10 @@
android:title="@string/settings"
android:showAsAction="never" />
<item android:id="@+id/menu_widget"
android:title="@string/widget"
android:showAsAction="never" />
<item android:id="@+id/menu_feedback"
android:title="@string/menu_feedback"
android:showAsAction="never" >

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_sort_by"
android:showAsAction="never"
android:title="@string/menu_sort_by">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/menu_sort_by_name"
android:title="@string/menu_sort_by_name" />
<item
android:id="@+id/menu_sort_by_subs"
android:title="@string/menu_sort_by_subs" />
<item
android:id="@+id/menu_sort_by_stories_month"
android:title="@string/menu_sort_by_stories_month" />
<item
android:id="@+id/menu_sort_by_recent_story"
android:title="@string/menu_sort_by_recent_story" />
<item
android:id="@+id/menu_sort_by_number_opens"
android:title="@string/menu_sort_by_number_opens" />
</group>
</menu>
</item>
<item
android:id="@+id/menu_sort_order"
android:showAsAction="never"
android:title="@string/menu_sort_order">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/menu_sort_order_ascending"
android:title="@string/menu_sort_order_ascending" />
<item
android:id="@+id/menu_sort_order_descending"
android:title="@string/menu_sort_order_descending" />
</group>
</menu>
</item>
<item
android:id="@+id/menu_folder_view"
android:showAsAction="never"
android:title="@string/menu_folder_view">
<menu>
<group android:checkableBehavior="single">
<item
android:id="@+id/menu_folder_view_nested"
android:title="@string/menu_folder_view_nested" />
<item
android:id="@+id/menu_folder_view_flat"
android:title="@string/menu_folder_view_flat" />
</group>
</menu>
</item>
<item
android:id="@+id/menu_select_all"
android:title="@string/menu_select_all" />
<item
android:id="@+id/menu_select_none"
android:title="@string/menu_select_none" />
</menu>

View file

@ -66,6 +66,14 @@
<color name="dark_story_content_text">#CECECE</color>
<color name="story_author_text">#959595</color>
<color name="widget_title">#8C8C8C</color>
<color name="widget_divider">#42453E</color>
<color name="widget_feed_title">#8C8C8C</color>
<color name="widget_story_title">#CECECE</color>
<color name="widget_story_content">#8C8C8C</color>
<color name="widget_story_author">#404040</color>
<color name="widget_story_date_time">#BDBDBD</color>
<color name="story_comment_divider">#F0F0F0</color>
<color name="dark_story_comment_divider">#42453E</color>
@ -130,4 +138,6 @@
<color name="refresh_3">#e9a941</color>
<color name="refresh_4">#fae576</color>
<color name="widget_background">#C8000000</color>
</resources>

View file

@ -28,12 +28,13 @@
<string name="description_login_logo">NewsBlur Logo</string>
<string name="description_profile_picture">The user\'s profile picture</string>
<string name="description_row_folder_icon">folder icon</string>
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
<string name="description_follow_button">Follow or unfollow a user</string>
<string name="description_comment_user">Comment user image</string>
<string name="description_empty_list_image">Empty list placeholder</string>
<string name="description_row_feed_icon">feed icon</string>
<string name="description_activity_icon">An icon illustrating the user\'s activity</string>
<string name="description_follow_button">Follow or unfollow a user</string>
<string name="description_comment_user">Comment user image</string>
<string name="description_empty_list_image">Empty list placeholder</string>
<string name="description_menu">Menu</string>
<string name="title_choose_folders">Choose Folders for Feed %s</string>
<string name="title_rename_feed">Rename Feed %s</string>
@ -165,6 +166,21 @@
<string name="list_style_grid_m">Grid (medium)</string>
<string name="list_style_grid_c">Grid (coarse)</string>
<string name="menu_sort_by">Sort By</string>
<string name="menu_sort_by_name">Name</string>
<string name="menu_sort_by_subs">Subscribers</string>
<string name="menu_sort_by_stories_month">Stories Per Month</string>
<string name="menu_sort_by_recent_story">Most Recent Story</string>
<string name="menu_sort_by_number_opens">Number of opens</string>
<string name="menu_sort_order">Sort Order</string>
<string name="menu_sort_order_ascending">Ascending</string>
<string name="menu_sort_order_descending">Descending</string>
<string name="menu_folder_view">Folder View</string>
<string name="menu_folder_view_nested">Nested</string>
<string name="menu_folder_view_flat">Flat</string>
<string name="menu_select_all">Select All</string>
<string name="menu_select_none">Select None</string>
<string name="toast_hold_to_select">Press and hold to select text</string>
<string name="logout_warning">Are you sure you want to log out?</string>
@ -216,8 +232,14 @@
<string name="unknown_user">Unknown User</string>
<string name="delete_feed_message">Delete feed \&quot;%s\&quot;?</string>
<string name="unfollow_message">Unfollow \&quot;%s\&quot;?</string>
<string name="feed_subscribers">%s subscribers</string>
<string name="feed_opens">%d opens</string>
<string name="feed_stories_per_month">%d stories/month</string>
<string name="settings">Preferences</string>
<string name="widget">Widget</string>
<string name="title_widget_setup">Tap to setup in NewsBlur</string>
<string name="title_no_subscriptions">No active subscriptions detected</string>
<string name="settings_cat_offline">Offline</string>
<string name="settings_enable_offline">Download Stories</string>

View file

@ -1,12 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="180dp"
android:minWidth="320dp"
android:minHeight="100dp"
android:minResizeWidth="100dp"
android:minResizeHeight="60dp"
android:updatePeriodMillis="14400000"
android:initialLayout="@layout/newsblur_widget"
android:configure="com.newsblur.widget.ConfigureWidgetActivity"
android:updatePeriodMillis="1800000"
android:initialLayout="@layout/view_app_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen">
</appwidget-provider>

View file

@ -1,5 +1,6 @@
package com.newsblur.activity;
import android.content.Intent;
import android.os.Bundle;
import com.newsblur.R;
@ -14,4 +15,13 @@ public class AllStoriesItemsList extends ItemsList {
UIUtils.setCustomActionBar(this, R.drawable.ak_icon_allstories, getResources().getString(R.string.all_stories_title));
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
UIUtils.startReadingActivity(fs, hash, true,this);
}
}
}

View file

@ -40,7 +40,8 @@ import com.newsblur.util.UIUtils;
public abstract class ItemsList extends NbActivity implements StoryOrderChangedListener, ReadFilterChangedListener, OnSeekBarChangeListener {
public static final String EXTRA_FEED_SET = "feed_set";
public static final String EXTRA_STORY_HASH = "story_hash";
public static final String EXTRA_WIDGET_STORY = "widget_story";
private static final String STORY_ORDER = "storyOrder";
private static final String READ_FILTER = "readFilter";
private static final String DEFAULT_FEED_VIEW = "defaultFeedView";
@ -67,9 +68,12 @@ public abstract class ItemsList extends NbActivity implements StoryOrderChangedL
// reduce UI lag, or in case somehow we got redisplayed in a zero-story state
FeedUtils.prepareReadingSession(fs, false);
if (PrefsUtils.isAutoOpenFirstUnread(this)) {
if (getIntent().getBooleanExtra(EXTRA_WIDGET_STORY, false)) {
String hash = (String) getIntent().getSerializableExtra(EXTRA_STORY_HASH);
UIUtils.startReadingActivity(fs, hash, true, this);
} else if (PrefsUtils.isAutoOpenFirstUnread(this)) {
if (FeedUtils.dbHelper.getUnreadCount(fs, intelState) > 0) {
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, this);
UIUtils.startReadingActivity(fs, Reading.FIND_FIRST_UNREAD, false,this);
}
}

View file

@ -44,6 +44,7 @@ import com.newsblur.util.PrefsUtils;
import com.newsblur.util.StateFilter;
import com.newsblur.util.UIUtils;
import com.newsblur.view.StateToggleButton.StateChangedListener;
import com.newsblur.widget.WidgetUtils;
public class Main extends NbActivity implements StateChangedListener, SwipeRefreshLayout.OnRefreshListener, AbsListView.OnScrollListener, PopupMenu.OnMenuItemClickListener, OnSeekBarChangeListener {
@ -294,6 +295,8 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
} else if (themeValue == ThemeValue.BLACK) {
menu.findItem(R.id.menu_theme_black).setChecked(true);
}
menu.findItem(R.id.menu_widget).setVisible(WidgetUtils.hasActiveAppWidgets(this));
pm.setOnMenuItemClickListener(this);
pm.show();
@ -324,7 +327,11 @@ public class Main extends NbActivity implements StateChangedListener, SwipeRefre
Intent settingsIntent = new Intent(this, Settings.class);
startActivity(settingsIntent);
return true;
} else if (item.getItemId() == R.id.menu_feedback_email) {
} else if (item.getItemId() == R.id.menu_widget) {
Intent widgetIntent = new Intent(this, WidgetConfig.class);
startActivity(widgetIntent);
return true;
} else if (item.getItemId() == R.id.menu_feedback_email) {
PrefsUtils.sendLogEmail(this);
return true;
} else if (item.getItemId() == R.id.menu_feedback_post) {

View file

@ -53,6 +53,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
public static final String EXTRA_FEEDSET = "feed_set";
public static final String EXTRA_POSITION = "feed_position";
public static final String EXTRA_STORY_HASH = "story_hash";
public static final String EXTRA_WAIT_REFRESH = "wait_refresh";
private static final String BUNDLE_POSITION = "position";
private static final String BUNDLE_STARTING_UNREAD = "starting_unread";
private static final String BUNDLE_SELECTED_FEED_VIEW = "selectedFeedView";
@ -104,6 +105,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
private int lastVScrollPos = 0;
private boolean unreadSearchActive = false;
private boolean waitRefresh = false;
private List<Story> pageHistory;
@ -124,6 +126,7 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
try {
fs = (FeedSet)getIntent().getSerializableExtra(EXTRA_FEEDSET);
waitRefresh = getIntent().getBooleanExtra(EXTRA_WAIT_REFRESH, false);
} catch (RuntimeException re) {
// in the wild, the notification system likes to pass us an Intent that has missing or very stale
// Serializable extras.
@ -315,6 +318,11 @@ public abstract class Reading extends NbActivity implements OnPageChangeListener
emptyViewText.setVisibility(View.INVISIBLE);
storyHash = null;
return;
} else if (waitRefresh) {
// when deep linking from app widget the db might not have the latest stories
// that we're looking to read so return and wait for updated cursor
waitRefresh = false;
return;
}
// if the story wasn't found, try to get more stories into the cursor

View file

@ -0,0 +1,275 @@
package com.newsblur.activity;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ExpandableListView;
import android.widget.TextView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import butterknife.Bind;
import butterknife.ButterKnife;
public class WidgetConfig extends NbActivity {
@Bind(R.id.list_view)
ExpandableListView listView;
@Bind(R.id.text_no_subscriptions)
TextView textNoSubscriptions;
private WidgetConfigAdapter adapter;
private ArrayList<Feed> feeds;
private ArrayList<Folder> folders;
private Map<String, Feed> feedMap = new HashMap<>();
private ArrayList<String> folderNames = new ArrayList<>();
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_widget_config);
ButterKnife.bind(this);
getActionBar().setDisplayHomeAsUpEnabled(true);
setupList();
loadFeeds();
loadFolders();
}
@Override
protected void onPause() {
super.onPause();
// notify widget to refresh next time it's viewed
WidgetUtils.notifyViewDataChanged(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_widget, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
ListOrderFilter listOrderFilter = PrefsUtils.getWidgetConfigListOrder(this);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
menu.findItem(R.id.menu_sort_order_ascending).setChecked(true);
} else if (listOrderFilter == ListOrderFilter.DESCENDING) {
menu.findItem(R.id.menu_sort_order_descending).setChecked(true);
}
FeedOrderFilter feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(this);
if (feedOrderFilter == FeedOrderFilter.NAME) {
menu.findItem(R.id.menu_sort_by_name).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
menu.findItem(R.id.menu_sort_by_subs).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
menu.findItem(R.id.menu_sort_by_stories_month).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY) {
menu.findItem(R.id.menu_sort_by_recent_story).setChecked(true);
} else if (feedOrderFilter == FeedOrderFilter.OPENS) {
menu.findItem(R.id.menu_sort_by_number_opens).setChecked(true);
}
FolderViewFilter folderViewFilter = PrefsUtils.getWidgetConfigFolderView(this);
if (folderViewFilter == FolderViewFilter.NESTED) {
menu.findItem(R.id.menu_folder_view_nested).setChecked(true);
} else if (folderViewFilter == FolderViewFilter.FLAT) {
menu.findItem(R.id.menu_folder_view_flat).setChecked(true);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_sort_order_ascending:
replaceListOrderFilter(ListOrderFilter.ASCENDING);
return true;
case R.id.menu_sort_order_descending:
replaceListOrderFilter(ListOrderFilter.DESCENDING);
return true;
case R.id.menu_sort_by_name:
replaceFeedOrderFilter(FeedOrderFilter.NAME);
return true;
case R.id.menu_sort_by_subs:
replaceFeedOrderFilter(FeedOrderFilter.SUBSCRIBERS);
return true;
case R.id.menu_sort_by_recent_story:
replaceFeedOrderFilter(FeedOrderFilter.RECENT_STORY);
return true;
case R.id.menu_sort_by_stories_month:
replaceFeedOrderFilter(FeedOrderFilter.STORIES_MONTH);
return true;
case R.id.menu_sort_by_number_opens:
replaceFeedOrderFilter(FeedOrderFilter.OPENS);
return true;
case R.id.menu_folder_view_nested:
replaceFolderView(FolderViewFilter.NESTED);
return true;
case R.id.menu_folder_view_flat:
replaceFolderView(FolderViewFilter.FLAT);
return true;
case R.id.menu_select_all:
selectAllFeeds();
return true;
case R.id.menu_select_none:
replaceWidgetFeedIds(Collections.<String>emptySet());
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void setupList() {
adapter = new WidgetConfigAdapter(this);
listView.setAdapter(adapter);
}
private void loadFeeds() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
processFeeds(cursor);
}
});
loader.startLoading();
}
private void loadFolders() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
processFolders(cursor);
}
});
loader.startLoading();
}
private void processFeeds(Cursor cursor) {
ArrayList<Feed> feeds = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Feed feed = Feed.fromCursor(cursor);
if (feed.active) {
feeds.add(feed);
feedMap.put(feed.feedId, feed);
}
}
this.feeds = feeds;
processData();
}
private void processFolders(Cursor cursor) {
ArrayList<Folder> folders = new ArrayList<>();
while (cursor != null && cursor.moveToNext()) {
Folder folder = Folder.fromCursor(cursor);
if (!folder.feedIds.isEmpty()) {
folders.add(folder);
}
}
this.folders = folders;
Collections.sort(this.folders, new Comparator<Folder>() {
@Override
public int compare(Folder o1, Folder o2) {
return Folder.compareFolderNames(o1.flatName(), o2.flatName());
}
});
processData();
}
private void processData() {
if (folders != null && feeds != null) {
for (Folder folder : folders) {
ArrayList<Feed> activeFeeds = new ArrayList<>();
for (String feedId : folder.feedIds) {
Feed feed = feedMap.get(feedId);
if (feed != null && feed.active && !activeFeeds.contains(feed)) {
activeFeeds.add(feed);
}
}
folderNames.add(folder.flatName());
folderChildren.add(activeFeeds);
}
setSelectedFeeds();
setAdapterData();
}
}
private void selectAllFeeds() {
Set<String> feedIds = new HashSet<>(this.feeds.size());
for (Feed feed : this.feeds) {
feedIds.add(feed.feedId);
}
replaceWidgetFeedIds(feedIds);
}
private void replaceWidgetFeedIds(Set<String> feedIds) {
PrefsUtils.setWidgetFeedIds(this, feedIds);
adapter.replaceFeedIds(feedIds);
}
private void replaceFeedOrderFilter(FeedOrderFilter feedOrderFilter) {
PrefsUtils.setWidgetConfigFeedOrder(this, feedOrderFilter);
adapter.replaceFeedOrder(feedOrderFilter);
}
private void replaceListOrderFilter(ListOrderFilter listOrderFilter) {
PrefsUtils.setWidgetConfigListOrder(this, listOrderFilter);
adapter.replaceListOrder(listOrderFilter);
}
private void replaceFolderView(FolderViewFilter folderViewFilter) {
PrefsUtils.setWidgetConfigFolderView(this, folderViewFilter);
adapter.replaceFolderView(folderViewFilter);
setAdapterData();
}
private void setSelectedFeeds() {
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(this);
// by default select all feeds
if (feedIds == null) {
feedIds = new HashSet<>(this.feeds.size());
for (Feed feed : this.feeds) {
feedIds.add(feed.feedId);
}
}
adapter.setFeedIds(feedIds);
}
private void setAdapterData() {
adapter.setData(this.folderNames, this.folderChildren, this.feeds);
listView.setVisibility(this.feeds.isEmpty() ? View.GONE : View.VISIBLE);
textNoSubscriptions.setVisibility(this.feeds.isEmpty() ? View.VISIBLE : View.GONE);
}
}

View file

@ -0,0 +1,296 @@
package com.newsblur.activity;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.CheckBox;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.TextView;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.util.AppConstants;
import com.newsblur.util.FeedOrderFilter;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.FolderViewFilter;
import com.newsblur.util.ListOrderFilter;
import com.newsblur.util.PrefsUtils;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;
public class WidgetConfigAdapter extends BaseExpandableListAdapter {
private final static int defaultTextSizeChild = 14;
private final static int defaultTextSizeGroup = 13;
private Set<String> feedIds = new HashSet<>();
private ArrayList<String> folderNames = new ArrayList<>();
private ArrayList<ArrayList<Feed>> folderChildren = new ArrayList<>();
private FolderViewFilter folderViewFilter;
private ListOrderFilter listOrderFilter;
private FeedOrderFilter feedOrderFilter;
private float textSize;
WidgetConfigAdapter(Context context) {
folderViewFilter = PrefsUtils.getWidgetConfigFolderView(context);
listOrderFilter = PrefsUtils.getWidgetConfigListOrder(context);
feedOrderFilter = PrefsUtils.getWidgetConfigFeedOrder(context);
textSize = PrefsUtils.getListTextSize(context);
}
@Override
public int getGroupCount() {
return folderNames.size();
}
@Override
public int getChildrenCount(int groupPosition) {
return folderChildren.get(groupPosition).size();
}
@Override
public String getGroup(int groupPosition) {
return folderNames.get(groupPosition);
}
@Override
public Feed getChild(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition);
}
@Override
public long getGroupId(int groupPosition) {
return folderNames.get(groupPosition).hashCode();
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return folderChildren.get(groupPosition).get(childPosition).hashCode();
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public View getGroupView(final int groupPosition, boolean isExpanded, View convertView, final ViewGroup parent) {
String folderName = folderNames.get(groupPosition);
if (folderName.equals(AppConstants.ROOT_FOLDER)) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_root_folder, parent, false);
} else {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_folder, parent, false);
TextView textName = convertView.findViewById(R.id.text_folder_name);
textName.setTextSize(textSize * defaultTextSizeGroup);
textName.setText(folderName);
}
((ExpandableListView) parent).expandGroup(groupPosition);
convertView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ArrayList<Feed> folderChild = WidgetConfigAdapter.this.folderChildren.get(groupPosition);
// check all is selected
boolean allSelected = true;
for (Feed feed : folderChild) {
if (!feedIds.contains(feed.feedId)) {
allSelected = false;
break;
}
}
for (Feed feed : folderChild) {
if (allSelected) {
feedIds.remove(feed.feedId);
} else {
feedIds.add(feed.feedId);
}
}
setWidgetFeedIds(parent.getContext());
notifyDataChanged();
}
});
return convertView;
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, final ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.row_widget_config_feed, parent, false);
}
final Feed feed = folderChildren.get(groupPosition).get(childPosition);
TextView textTitle = convertView.findViewById(R.id.text_title);
TextView textDetails = convertView.findViewById(R.id.text_details);
final CheckBox checkBox = convertView.findViewById(R.id.check_box);
ImageView img = convertView.findViewById(R.id.img);
textTitle.setTextSize(textSize * defaultTextSizeChild);
textDetails.setTextSize(textSize * defaultTextSizeChild);
textTitle.setText(feed.title);
checkBox.setChecked(feedIds.contains(feed.feedId));
if (feedOrderFilter == FeedOrderFilter.NAME || feedOrderFilter == FeedOrderFilter.OPENS) {
textDetails.setText(parent.getContext().getString(R.string.feed_opens, feed.feedOpens));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS) {
textDetails.setText(parent.getContext().getString(R.string.feed_subscribers, feed.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH) {
textDetails.setText(parent.getContext().getString(R.string.feed_stories_per_month, feed.storiesPerMonth));
} else {
// FeedOrderFilter.RECENT_STORY
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date dateTime = dateFormat.parse(feed.lastStoryDate);
CharSequence relativeTimeString = DateUtils.getRelativeTimeSpanString(dateTime.getTime(), System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS);
textDetails.setText(relativeTimeString);
} catch (Exception e) {
textDetails.setText(feed.lastStoryDate);
}
}
FeedUtils.iconLoader.displayImage(feed.faviconUrl, img, 0, false, img.getHeight(), true);
convertView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
checkBox.setChecked(!checkBox.isChecked());
if (checkBox.isChecked()) {
feedIds.add(feed.feedId);
} else {
feedIds.remove(feed.feedId);
}
setWidgetFeedIds(parent.getContext());
}
});
return convertView;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public boolean areAllItemsEnabled() {
return super.areAllItemsEnabled();
}
void setData(ArrayList<String> activeFoldersNames, ArrayList<ArrayList<Feed>> activeFolderChildren, ArrayList<Feed> feeds) {
if (folderViewFilter == FolderViewFilter.NESTED) {
this.folderNames = activeFoldersNames;
this.folderChildren = activeFolderChildren;
} else {
this.folderNames = new ArrayList<>(1);
this.folderNames.add(AppConstants.ROOT_FOLDER);
this.folderChildren = new ArrayList<>();
this.folderChildren.add(feeds);
}
this.notifyDataChanged();
}
void replaceFeedOrder(FeedOrderFilter feedOrderFilter) {
this.feedOrderFilter = feedOrderFilter;
notifyDataChanged();
}
void replaceListOrder(ListOrderFilter listOrderFilter) {
this.listOrderFilter = listOrderFilter;
notifyDataChanged();
}
void replaceFolderView(FolderViewFilter folderViewFilter) {
this.folderViewFilter = folderViewFilter;
}
private void notifyDataChanged() {
for (ArrayList<Feed> feedList : this.folderChildren) {
Collections.sort(feedList, getListComparator());
}
this.notifyDataSetChanged();
}
void setFeedIds(Set<String> feedIds) {
this.feedIds.clear();
this.feedIds.addAll(feedIds);
}
void replaceFeedIds(Set<String> feedIds) {
setFeedIds(feedIds);
this.notifyDataSetChanged();
}
private void setWidgetFeedIds(Context context) {
PrefsUtils.setWidgetFeedIds(context, feedIds);
}
private Comparator<Feed> getListComparator() {
return new Comparator<Feed>() {
@Override
public int compare(Feed o1, Feed o2) {
if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.ASCENDING) {
return o1.title.compareTo(o2.title);
} else if (feedOrderFilter == FeedOrderFilter.NAME && listOrderFilter == ListOrderFilter.DESCENDING) {
return o2.title.compareTo(o1.title);
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.valueOf(o1.subscribers).compareTo(Integer.valueOf(o2.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.SUBSCRIBERS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.valueOf(o2.subscribers).compareTo(Integer.valueOf(o1.subscribers));
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.feedOpens, o2.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.OPENS && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.feedOpens, o1.feedOpens);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.ASCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.RECENT_STORY && listOrderFilter == ListOrderFilter.DESCENDING) {
return compareLastStoryDateTimes(o1.lastStoryDate, o2.lastStoryDate, listOrderFilter);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.ASCENDING) {
return Integer.compare(o1.storiesPerMonth, o2.storiesPerMonth);
} else if (feedOrderFilter == FeedOrderFilter.STORIES_MONTH && listOrderFilter == ListOrderFilter.DESCENDING) {
return Integer.compare(o2.storiesPerMonth, o1.storiesPerMonth);
}
return o1.title.compareTo(o2.title);
}
};
}
private int compareLastStoryDateTimes(String firstDateTime, String secondDateTime, ListOrderFilter listOrderFilter) {
try {
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
// found null last story date times on feeds
if (TextUtils.isEmpty(firstDateTime)) {
firstDateTime = "2000-01-01 00:00:00";
}
if (TextUtils.isEmpty(secondDateTime)) {
secondDateTime = "2000-01-01 00:00:00";
}
Date firstDate = dateFormat.parse(firstDateTime);
Date secondDate = dateFormat.parse(secondDateTime);
if (listOrderFilter == ListOrderFilter.ASCENDING) {
return firstDate.compareTo(secondDate);
} else {
return secondDate.compareTo(firstDate);
}
} catch (ParseException e) {
e.printStackTrace();
return 0;
}
}
}

View file

@ -6,6 +6,7 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.AsyncTask;
import android.os.CancellationSignal;
import android.support.annotation.Nullable;
import android.support.v4.content.AsyncTaskLoader;
import android.support.v4.content.Loader;
import android.text.TextUtils;
@ -1106,8 +1107,7 @@ public class BlurDatabaseHelper {
};
}
public Loader<Cursor> getStoriesLoader(final FeedSet fs) {
// final StoryOrder order = PrefsUtils.getStoryOrder(context, fs);
public Loader<Cursor> getStoriesLoader(@Nullable final FeedSet fs) {
return new QueryCursorLoader(context) {
@Override
protected Cursor createCursor() {
@ -1116,15 +1116,21 @@ public class BlurDatabaseHelper {
};
}
private Cursor getStoriesCursor(FeedSet fs, CancellationSignal cancellationSignal) {
private Cursor getStoriesCursor(@Nullable FeedSet fs, CancellationSignal cancellationSignal) {
StringBuilder q = new StringBuilder(DatabaseConstants.STORY_QUERY_BASE_0);
q.append(DatabaseConstants.STORY_FEED_ID);
q.append(" = ");
q.append(fs.getSingleFeed());
if (fs != null && !TextUtils.isEmpty(fs.getSingleFeed())) {
q.append(DatabaseConstants.STORY_FEED_ID);
q.append(" = ");
q.append(fs.getSingleFeed());
} else {
q.append(DatabaseConstants.FEED_ACTIVE);
q.append(" = 1");
}
q.append(" ORDER BY ");
q.append(DatabaseConstants.STORY_TIMESTAMP);
q.append(" DESC LIMIT 10");
q.append(" DESC LIMIT 20");
return rawQuery(q.toString(), null, cancellationSignal);
}

View file

@ -35,6 +35,9 @@ public class DatabaseConstants {
public static final String FEED_LINK = "link";
public static final String FEED_ADDRESS = "address";
public static final String FEED_SUBSCRIBERS = "subscribers";
public static final String FEED_OPENS = "opens";
public static final String FEED_LAST_STORY_DATE = "last_story_date";
public static final String FEED_AVERAGE_STORIES_PER_MONTH = "average_stories_per_month";
public static final String FEED_UPDATED_SECONDS = "updated_seconds";
public static final String FEED_FAVICON_FADE = "favicon_fade";
public static final String FEED_FAVICON_COLOR = "favicon_color";
@ -183,6 +186,9 @@ public class DatabaseConstants {
FEED_LINK + TEXT + ", " +
FEED_SUBSCRIBERS + TEXT + ", " +
FEED_TITLE + TEXT + ", " +
FEED_OPENS + INTEGER + ", " +
FEED_AVERAGE_STORIES_PER_MONTH + INTEGER + ", " +
FEED_LAST_STORY_DATE + TEXT + ", " +
FEED_UPDATED_SECONDS + INTEGER + ", " +
FEED_NOTIFICATION_TYPES + TEXT + ", " +
FEED_NOTIFICATION_FILTER + TEXT + ", " +

View file

@ -349,7 +349,7 @@ public class StoryViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
return;
}
if (gestureL2R || gestureR2L) return;
UIUtils.startReadingActivity(fs, story.storyHash, context);
UIUtils.startReadingActivity(fs, story.storyHash, false, context);
}
@Override

View file

@ -54,6 +54,15 @@ public class Feed implements Comparable<Feed>, Serializable {
@SerializedName("num_subscribers")
public String subscribers;
@SerializedName("feed_opens")
public int feedOpens;
@SerializedName("last_story_date")
public String lastStoryDate;
@SerializedName("average_stories_per_month")
public int storiesPerMonth;
@SerializedName("feed_title")
public String title;
@ -85,6 +94,9 @@ public class Feed implements Comparable<Feed>, Serializable {
values.put(DatabaseConstants.FEED_FAVICON_URL, faviconUrl);
values.put(DatabaseConstants.FEED_LINK, feedLink);
values.put(DatabaseConstants.FEED_SUBSCRIBERS, subscribers);
values.put(DatabaseConstants.FEED_OPENS, feedOpens);
values.put(DatabaseConstants.FEED_LAST_STORY_DATE, lastStoryDate);
values.put(DatabaseConstants.FEED_AVERAGE_STORIES_PER_MONTH, storiesPerMonth);
values.put(DatabaseConstants.FEED_TITLE, title);
values.put(DatabaseConstants.FEED_UPDATED_SECONDS, lastUpdated);
values.put(DatabaseConstants.FEED_NOTIFICATION_TYPES, DatabaseConstants.flattenStringList(notificationTypes));
@ -113,6 +125,9 @@ public class Feed implements Comparable<Feed>, Serializable {
feed.neutralCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_NEUTRAL_COUNT));
feed.positiveCount = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_POSITIVE_COUNT));
feed.subscribers = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_SUBSCRIBERS));
feed.feedOpens = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_OPENS));
feed.storiesPerMonth = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_AVERAGE_STORIES_PER_MONTH));
feed.lastStoryDate = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_LAST_STORY_DATE));
feed.title = cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_TITLE));
feed.lastUpdated = cursor.getInt(cursor.getColumnIndex(DatabaseConstants.FEED_UPDATED_SECONDS));
feed.notificationTypes = DatabaseConstants.unflattenStringList(cursor.getString(cursor.getColumnIndex(DatabaseConstants.FEED_NOTIFICATION_TYPES)));

View file

@ -92,7 +92,7 @@ public class Folder {
* folder on top, and also the expectation that *despite locale*, folders
* starting with an underscore should show up on top.
*/
private static int compareFolderNames(String s1, String s2) {
public static int compareFolderNames(String s1, String s2) {
if (TextUtils.equals(s1, s2)) return 0;
if (s1.equals(AppConstants.ROOT_FOLDER)) return -1;
if (s2.equals(AppConstants.ROOT_FOLDER)) return 1;

View file

@ -153,7 +153,7 @@ public abstract class ProfileActivityDetailsFragment extends Fragment implements
//context.startActivity(intent);
}
} else if (activity.category == Category.STAR) {
UIUtils.startReadingActivity(FeedSet.allSaved(), activity.storyHash, context);
UIUtils.startReadingActivity(FeedSet.allSaved(), activity.storyHash, false, context);
} else if (isSocialFeedCategory(activity)) {
// Strip the social: prefix from feedId
String socialFeedId = activity.feedId.substring(7);
@ -161,7 +161,7 @@ public abstract class ProfileActivityDetailsFragment extends Fragment implements
if (feed == null) {
Toast.makeText(context, R.string.profile_do_not_follow, Toast.LENGTH_SHORT).show();
} else {
UIUtils.startReadingActivity(FeedSet.singleSocialFeed(feed.userId, feed.username), activity.storyHash, context);
UIUtils.startReadingActivity(FeedSet.singleSocialFeed(feed.userId, feed.username), activity.storyHash, false, context);
}
}
}

View file

@ -211,6 +211,7 @@ public class ReadingItemFragment extends NbFragment implements PopupMenu.OnMenuI
registerForContextMenu(web);
web.setCustomViewLayout(webviewCustomViewLayout);
web.setWebviewWrapperLayout(fragmentScrollview);
web.setBackgroundColor(Color.TRANSPARENT);
web.fragment = this;
web.activity = activity;

View file

@ -52,6 +52,7 @@ import com.newsblur.util.PrefConstants;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.widget.WidgetUtils;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
@ -296,7 +297,13 @@ public class APIManager {
ValueMultimap values = new ValueMultimap();
// create the URI and populate request params depending on what kind of stories we want
if (fs.getSingleFeed() != null) {
if (fs.isForWidget()) {
uri = Uri.parse(buildUrl(APIConstants.PATH_RIVER_STORIES));
for (String feedId : fs.getAllFeeds()) values.put(APIConstants.PARAMETER_FEEDS, feedId);
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_FALSE);
values.put(APIConstants.PARAMETER_INFREQUENT, APIConstants.VALUE_FALSE);
values.put(APIConstants.PARAMETER_LIMIT, String.valueOf(WidgetUtils.STORIES_LIMIT));
} else if (fs.getSingleFeed() != null) {
uri = Uri.parse(buildUrl(APIConstants.PATH_FEED_STORIES)).buildUpon().appendPath(fs.getSingleFeed()).build();
values.put(APIConstants.PARAMETER_FEEDS, fs.getSingleFeed());
values.put(APIConstants.PARAMETER_INCLUDE_HIDDEN, APIConstants.VALUE_TRUE);

View file

@ -8,10 +8,11 @@ import android.content.Context;
import android.content.Intent;
import com.newsblur.util.AppConstants;
import com.newsblur.widget.WidgetUtils;
/**
* First receiver in the chain that starts with the device. Simply schedules another broadcast
* that will periodicaly start the sync service.
* that will periodically start the sync service.
*/
public class BootReceiver extends BroadcastReceiver {
@ -19,6 +20,7 @@ public class BootReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
com.newsblur.util.Log.d(this, "triggering sync service from device boot");
scheduleSyncService(context);
resetWidgetSync(context);
}
public static void scheduleSyncService(Context context) {
@ -32,5 +34,9 @@ public class BootReceiver extends BroadcastReceiver {
int result = sched.schedule(builder.build());
com.newsblur.util.Log.d("BootReceiver", String.format("Scheduling result: %s - %s", result, result == 0 ? "Failure" : "Success"));
}
private static void resetWidgetSync(Context context) {
com.newsblur.util.Log.d(BootReceiver.class.getName(), "Received " + Intent.ACTION_BOOT_COMPLETED + " - reset widget sync");
WidgetUtils.resetWidgetUpdate(context);
}
}

View file

@ -3,8 +3,6 @@ package com.newsblur.service;
import android.app.Service;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
@ -39,7 +37,7 @@ import com.newsblur.util.ReadingAction;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StateFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.widget.NewsBlurWidgetProvider;
import com.newsblur.widget.WidgetUtils;
import java.util.ArrayList;
import java.util.Date;
@ -47,7 +45,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
@ -284,14 +281,11 @@ public class NBSyncService extends JobService {
// on all devices
housekeeping();
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this, NewsBlurWidgetProvider.class));
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
// check to see if we are on an allowable network only after ensuring we have CPU
if (!( (NbActivity.getActiveActivityCount() > 0) ||
PrefsUtils.isEnableNotifications(this) ||
PrefsUtils.isBackgroundNetworkAllowed(this) )) {
PrefsUtils.isBackgroundNetworkAllowed(this) ||
WidgetUtils.hasActiveAppWidgets(this)) ) {
Log.d(this.getClass().getName(), "Abandoning sync: app not active and network type not appropriate for background sync.");
return;
}

View file

@ -0,0 +1,20 @@
package com.newsblur.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import com.newsblur.widget.WidgetUtils;
public class TimeChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, @Nullable Intent intent) {
if (intent != null && intent.getAction() != null
&& intent.getAction().equals(Intent.ACTION_TIME_CHANGED)) {
com.newsblur.util.Log.d(TimeChangeReceiver.class.getName(), "Received " + Intent.ACTION_TIME_CHANGED + " - reset widget sync");
WidgetUtils.resetWidgetUpdate(context);
}
}
}

View file

@ -0,0 +1,9 @@
package com.newsblur.util;
public enum FeedOrderFilter {
NAME,
SUBSCRIBERS,
STORIES_MONTH,
RECENT_STORY,
OPENS
}

View file

@ -1,5 +1,6 @@
package com.newsblur.util;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.Serializable;
@ -29,6 +30,7 @@ public class FeedSet implements Serializable {
private boolean isAllRead;
private boolean isGlobalShared;
private boolean isInfrequent;
private boolean isForWidget;
private String folderName;
private String searchQuery;
@ -73,6 +75,17 @@ public class FeedSet implements Serializable {
return fs;
}
/**
* Convenience constructor for multiple feeds with IDs
*/
public static FeedSet multipleFeeds(Set<String> feedIds) {
FeedSet fs = new FeedSet();
fs.feeds = new HashSet<>(feedIds.size());
fs.feeds.addAll(feedIds);
fs.feeds = Collections.unmodifiableSet(fs.feeds);
return fs;
}
/**
* Convenience constructor for all (non-social) feeds.
*/
@ -138,6 +151,22 @@ public class FeedSet implements Serializable {
return fs;
}
/**
* Convenience constructor for widget feed.
*/
public static FeedSet widgetFeeds(@Nullable Set<String> feedIds){
FeedSet fs = new FeedSet();
fs.isForWidget = true;
if (feedIds != null) {
fs.feeds = new HashSet<>(feedIds.size());
fs.feeds.addAll(feedIds);
fs.feeds = Collections.unmodifiableSet(fs.feeds);
} else {
fs.feeds = Collections.EMPTY_SET;
}
return fs;
}
/**
* Convenience constructor for a folder.
*/
@ -163,6 +192,13 @@ public class FeedSet implements Serializable {
if (feeds != null && ((feeds.size() > 1) || (folderName != null))) return feeds; else return null;
}
/**
* Gets a set of all feed IDs if there are any or null otherwise.
*/
public Set<String> getAllFeeds() {
if (feeds != null) return feeds; else return null;
}
/**
* Gets a single social feed ID and username iff there is only one or null otherwise.
*/
@ -201,6 +237,10 @@ public class FeedSet implements Serializable {
return ((savedTags != null) && (savedTags.size() == 1));
}
public boolean isForWidget() {
return this.isForWidget;
}
/**
* Gets a single saved tag iff there is only one or null otherwise.
*/

View file

@ -0,0 +1,6 @@
package com.newsblur.util;
public enum FolderViewFilter {
FLAT,
NESTED
}

View file

@ -14,6 +14,7 @@ import android.graphics.BitmapFactory;
import android.os.Process;
import android.view.View;
import android.widget.ImageView;
import android.widget.RemoteViews;
import com.newsblur.R;
import com.newsblur.network.APIConstants;
@ -72,15 +73,48 @@ public class ImageLoader {
}
}
/**
* Synchronous background call coming from app widget on home screen
*/
public void displayWidgetImage(String url, int imageViewId, int maxDimPX, RemoteViews remoteViews) {
if (url == null) {
remoteViews.setViewVisibility(imageViewId, View.GONE);
return;
}
url = buildUrlIfNeeded(url);
// try from memory
Bitmap bitmap = memoryCache.get(url);
if (bitmap != null) {
remoteViews.setImageViewBitmap(imageViewId, bitmap);
remoteViews.setViewVisibility(imageViewId, View.VISIBLE);
return;
}
// try from disk
bitmap = getImageFromDisk(url, maxDimPX, false, 0f);
if (bitmap == null) {
// try for network
bitmap = getImageFromNetwork(url, maxDimPX,false, 0f);
}
if (bitmap != null) {
memoryCache.put(url, bitmap);
remoteViews.setImageViewBitmap(imageViewId, bitmap);
remoteViews.setViewVisibility(imageViewId, View.VISIBLE);
} else {
remoteViews.setViewVisibility(imageViewId, View.GONE);
}
}
public PhotoToLoad displayImage(String url, ImageView imageView, float roundRadius, boolean cropSquare, int maxDimPX, boolean allowDelay) {
if (url == null) {
imageView.setImageResource(emptyRID);
return null;
}
if (url.startsWith("/")) {
url = APIConstants.buildUrl(url);
}
url = buildUrlIfNeeded(url);
imageViewMappings.put(imageView, url);
PhotoToLoad photoToLoad = new PhotoToLoad(url, imageView, roundRadius, cropSquare, maxDimPX, allowDelay);
@ -150,16 +184,11 @@ public class ImageLoader {
}
// try from disk
File f = fileCache.getCachedFile(photoToLoad.url);
// the only reliable way to check a cached file is to try decoding it. the util method will
// return null if it fails
bitmap = UIUtils.decodeImage(f, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
// try for network
bitmap = getImageFromDisk(photoToLoad.url, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
if (bitmap == null) {
// try for network
if (photoToLoad.cancel) return;
fileCache.cacheFile(photoToLoad.url);
f = fileCache.getCachedFile(photoToLoad.url);
bitmap = UIUtils.decodeImage(f, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
bitmap = getImageFromNetwork(photoToLoad.url, photoToLoad.maxDimPX, photoToLoad.cropSquare, photoToLoad.roundRadius);
}
if (bitmap != null) {
@ -227,4 +256,23 @@ public class ImageLoader {
return true;
}
}
private String buildUrlIfNeeded(String url) {
if (url.startsWith("/")) {
url = APIConstants.buildUrl(url);
}
return url;
}
private Bitmap getImageFromDisk(String url, int maxDimPX, boolean cropSquare, float roundRadius) {
// the only reliable way to check a cached file is to try decoding it. the util method will
// return null if it fails
File f = fileCache.getCachedFile(url);
return UIUtils.decodeImage(f, maxDimPX, cropSquare, roundRadius);
}
private Bitmap getImageFromNetwork(String url, int maxDimPX, boolean cropSquare, float roundRadius) {
fileCache.cacheFile(url);
File f = fileCache.getCachedFile(url);
return UIUtils.decodeImage(f, maxDimPX, cropSquare, roundRadius);
}
}

View file

@ -0,0 +1,6 @@
package com.newsblur.util;
public enum ListOrderFilter {
ASCENDING,
DESCENDING
}

View file

@ -106,6 +106,8 @@ public class PrefConstants {
public static final String ENABLE_NOTIFICATIONS = "enable_notifications";
public static final String READING_FONT = "reading_font";
public static final String WIDGET_FEED_ID = "WIDGET_FEED_ID";
public static final String WIDGET_FEED_NAME = "WIDGET_FEED_NAME";
public static final String WIDGET_FEED_SET = "widget_feed_set";
public static final String WIDGET_CONFIG_LIST_ORDER = "widget_config_list_order";
public static final String WIDGET_CONFIG_FEED_ORDER = "widget_config_feed_order";
public static final String WIDGET_CONFIG_FOLDER_VIEW = "widget_config_folder_view";
}

View file

@ -21,6 +21,7 @@ import android.graphics.BitmapFactory;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import android.util.Log;
@ -30,6 +31,7 @@ import com.newsblur.domain.UserDetails;
import com.newsblur.network.APIConstants;
import com.newsblur.util.PrefConstants.ThemeValue;
import com.newsblur.service.NBSyncService;
import com.newsblur.widget.WidgetUtils;
public class PrefsUtils {
@ -161,6 +163,9 @@ public class PrefsUtils {
// wipe the local DB
FeedUtils.dropAndRecreateTables();
// disable widget
WidgetUtils.disableWidgetUpdate(context);
// reset custom server
APIConstants.unsetCustomServer();
@ -840,7 +845,7 @@ public class PrefsUtils {
}
public static boolean isBackgroundNeeded(Context context) {
return (isEnableNotifications(context) || isOfflineEnabled(context));
return (isEnableNotifications(context) || isOfflineEnabled(context) || WidgetUtils.hasActiveAppWidgets(context));
}
public static Font getFont(Context context) {
@ -859,42 +864,70 @@ public class PrefsUtils {
editor.commit();
}
public static void setWidgetFeed(Context context, int widgetId, String feedId, String name) {
public static void setWidgetFeedIds(Context context, Set<String> feedIds) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
editor.putString(PrefConstants.WIDGET_FEED_ID + widgetId, feedId)
.putString(PrefConstants.WIDGET_FEED_NAME + widgetId, name);
editor.putStringSet(PrefConstants.WIDGET_FEED_SET, feedIds);
editor.commit();
}
/**
* sets only the name, no id when it is a folder
*/
public static void setWidgetFolderName(Context context, int widgetId, String folderName) {
@Nullable
public static Set<String> getWidgetFeedIds(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getStringSet(PrefConstants.WIDGET_FEED_SET, null);
}
public static void removeWidgetData(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
editor.remove(PrefConstants.WIDGET_FEED_ID + widgetId)
.putString(PrefConstants.WIDGET_FEED_NAME + widgetId, folderName);
editor.commit();
}
public static String getWidgetFeed(Context context, int widgetId) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
String feedId = preferences.getString(PrefConstants.WIDGET_FEED_ID + widgetId, null);
return feedId;
}
public static String getWidgetFeedName(Context context, int widgetId) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return preferences.getString(PrefConstants.WIDGET_FEED_NAME + widgetId, "-");
}
public static void removeWidgetFeed(Context context, int widgetId) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
if(prefs.contains(PrefConstants.WIDGET_FEED_ID + widgetId)){
Editor editor = prefs.edit();
editor.remove(PrefConstants.WIDGET_FEED_ID + widgetId);
editor.remove(PrefConstants.WIDGET_FEED_NAME + widgetId);
editor.apply();
if (prefs.contains(PrefConstants.WIDGET_FEED_SET)) {
editor.remove(PrefConstants.WIDGET_FEED_SET);
}
if (prefs.contains(PrefConstants.WIDGET_CONFIG_FEED_ORDER)) {
editor.remove(PrefConstants.WIDGET_CONFIG_FEED_ORDER);
}
if (prefs.contains(PrefConstants.WIDGET_CONFIG_LIST_ORDER)) {
editor.remove(PrefConstants.WIDGET_CONFIG_LIST_ORDER);
}
if (prefs.contains(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW)) {
editor.remove(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW);
}
editor.apply();
}
public static FeedOrderFilter getWidgetConfigFeedOrder(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return FeedOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, FeedOrderFilter.NAME.toString()));
}
public static void setWidgetConfigFeedOrder(Context context, FeedOrderFilter feedOrderFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
editor.putString(PrefConstants.WIDGET_CONFIG_FEED_ORDER, feedOrderFilter.toString());
editor.commit();
}
public static ListOrderFilter getWidgetConfigListOrder(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return ListOrderFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, ListOrderFilter.ASCENDING.name()));
}
public static void setWidgetConfigListOrder(Context context, ListOrderFilter listOrderFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
editor.putString(PrefConstants.WIDGET_CONFIG_LIST_ORDER, listOrderFilter.toString());
editor.commit();
}
public static FolderViewFilter getWidgetConfigFolderView(Context context) {
SharedPreferences preferences = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
return FolderViewFilter.valueOf(preferences.getString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, FolderViewFilter.NESTED.name()));
}
public static void setWidgetConfigFolderView(Context context, FolderViewFilter folderViewFilter) {
SharedPreferences prefs = context.getSharedPreferences(PrefConstants.PREFERENCES, 0);
Editor editor = prefs.edit();
editor.putString(PrefConstants.WIDGET_CONFIG_FOLDER_VIEW, folderViewFilter.toString());
editor.commit();
}
}

View file

@ -270,7 +270,7 @@ public class UIUtils {
});
}
public static void startReadingActivity(FeedSet fs, String startingHash, Context context) {
public static void startReadingActivity(FeedSet fs, String startingHash, boolean waitRefresh, Context context) {
Class activityClass;
if (fs.isAllSaved()) {
activityClass = SavedStoriesReading.class;
@ -299,6 +299,7 @@ public class UIUtils {
Intent i = new Intent(context, activityClass);
i.putExtra(Reading.EXTRA_FEEDSET, fs);
i.putExtra(Reading.EXTRA_STORY_HASH, startingHash);
i.putExtra(Reading.EXTRA_WAIT_REFRESH, waitRefresh);
context.startActivity(i);
}

View file

@ -1,194 +0,0 @@
package com.newsblur.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import com.newsblur.R;
import com.newsblur.domain.Story;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.StoryUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BlurWidgetRemoteViewsService extends RemoteViewsService {
private static String TAG = "BlurWidgetRemoteViewsFactory";
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
Log.d(TAG, "onGetViewFactory");
return new BlurWidgetRemoteViewsFactory(this.getApplicationContext(), intent);
}
}
class BlurWidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private Context context;
private static String TAG = "BlurWidgetRemoteViewsFactory";
private List<Story> storyItems = new ArrayList<Story>();
private FeedSet fs;
private APIManager apiManager;
public BlurWidgetRemoteViewsFactory(Context context, Intent intent) {
Log.d(TAG, "Constructor");
this.context = context;
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
final String feedId = PrefsUtils.getWidgetFeed(context, appWidgetId);
final String feedName = PrefsUtils.getWidgetFeedName(context, appWidgetId);
apiManager = new APIManager(context);
Log.d(TAG, "Feed ID: " + feedId);
if(feedId != null){
// this is a single feed
fs = FeedSet.singleFeed(feedId);
}else{
// this is a folder
fs = FeedUtils.feedSetFromFolderName(feedName);
}
}
/**
* The system calls onCreate() when creating your factory for the first time.
* This is where you set up any connections and/or cursors to your data source.
*
* Heavy lifting,
* for example downloading or creating content etc, should be deferred to onDataSetChanged()
* or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
*/
@Override
public void onCreate() {
Log.d(TAG, "onCreate");
}
private void fetchStories() {
Log.d(TAG, String.format("Fetching stories %s", fs.hashCode()));
StoriesResponse response =
apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL);
if (response == null) {
Log.e(TAG, "Response is null");
return;
} else if (response.stories == null) {
Log.e(TAG, "Stories are empty");
return;
} else if (response.isError()) {
String err = String.format("response error for feed %s", fs.hashCode());
Log.e(TAG, response.getErrorMessage(err));
return;
}
storyItems.clear();
storyItems.addAll(Arrays.asList(response.stories));
}
/**
* allowed to run synchronous calls
*/
@Override
public RemoteViews getViewAt(int position) {
Log.d(TAG, "getViewAt " + position);
Story story = storyItems.get(position);
// Construct a remote views item based on the app widget item XML file,
// and set the text based on the position.
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.newsblur_widget_item);
rv.setTextViewText(R.id.widget_item_title, story.title);
CharSequence time = StoryUtils.formatRelativeTime(context, story.timestamp);
rv.setTextViewText(R.id.widget_item_time, time);
// Next, set a fill-intent, which will be used to fill in the pending intent template
// that is set on the collection view in StackWidgetProvider.
Bundle extras = new Bundle();
extras.putString(NewsBlurWidgetProvider.EXTRA_ITEM_ID, story.storyHash);
extras.putString(NewsBlurWidgetProvider.EXTRA_FEED_ID, story.feedId);
Intent fillInIntent = new Intent();
// fillInIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
fillInIntent.putExtras(extras);
// Make it possible to distinguish the individual on-click
// action of a given item
rv.setOnClickFillInIntent(R.id.newsblur_widget_item, fillInIntent);
return rv;
}
/**
* This allows for the use of a custom loading view which appears between the time that
* {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
* view will be used.
*
* @return The RemoteViews representing the desired loading view.
*/
@Override
public RemoteViews getLoadingView() {
return null;
}
/**
*
* @return The number of types of Views that will be returned by this factory.
*/
@Override
public int getViewTypeCount() {
return 1;
}
/**
*
* @param position The position of the item within the data set whose row id we want.
* @return The id of the item at the specified position.
*/
@Override
public long getItemId(int position) {
return storyItems.get(position).hashCode();
}
/**
*
* @return True if the same id always refers to the same object.
*/
@Override
public boolean hasStableIds() {
return true;
}
@Override
public void onDataSetChanged() {
// fetch any new data
fetchStories();
Log.d(TAG, "onDataSetChanged");
}
/**
* Called when the last RemoteViewsAdapter that is associated with this factory is
* unbound.
*/
@Override
public void onDestroy() {
Log.d(TAG, "onDestroy");
}
/**
*
* @return Count of items.
*/
@Override
public int getCount() {
Log.d(TAG, "getCount: " + Math.min(storyItems.size(), 10));
return Math.min(storyItems.size(), 10);
}
}

View file

@ -1,194 +0,0 @@
package com.newsblur.widget;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.widget.RemoteViews;
import com.newsblur.R;
import com.newsblur.activity.NbActivity;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Folder;
import com.newsblur.network.APIManager;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ConfigureWidgetActivity extends NbActivity {
private int appWidgetId;
private List<Feed> feeds = new ArrayList<>();
private List<Folder> folders = new ArrayList<>();
private static String TAG = "ConfigureWidgetActivity";
private Feed selectedFeed = null;
private Folder selectedFolder = null;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_configure_widget);
PrefsUtils.applyThemePreference(this);
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
appWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
PrefsUtils.removeWidgetFeed(this, appWidgetId);
folders = null;
feeds = null;
getAllFeeds();
getAllFolders();
// set result as cancelled in the case that we don't finish config
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_CANCELED, resultValue);
}
private void getAllFeeds() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
processFeeds(data);
}
});
loader.startLoading();
}
private void getAllFolders() {
Loader<Cursor> loader = FeedUtils.dbHelper.getFoldersLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor data) {
processFolders(data);
}
});
loader.startLoading();
}
private void processFolders(Cursor cursor) {
List<Folder> folders = new ArrayList<>();
while (cursor.moveToNext()) {
Folder f = Folder.fromCursor(cursor);
folders.add(f);
}
Collections.sort(folders, new Comparator<Folder>() {
@Override
public int compare(Folder o1, Folder o2) {
return o1.name.compareTo(o2.name);
}
});
this.folders = new ArrayList<>();
this.folders.addAll(folders);
requestFeedFromUser();
}
private void processFeeds(Cursor cursor) {
List<Feed> feeds = new ArrayList<>();
while (cursor.moveToNext()) {
Feed f = Feed.fromCursor(cursor);
if (f.active) {
feeds.add(f);
}
}
this.feeds = new ArrayList<>();
this.feeds.addAll(feeds);
requestFeedFromUser();
}
private void requestFeedFromUser() {
if (feeds == null || folders == null) {
return;
}
ArrayList<String> feedTitles = new ArrayList<>();
for (Folder folder : folders) {
feedTitles.add(String.format("Folder: %s", folder.name));
}
for (Feed feed : feeds) {
feedTitles.add(String.format("Feed: %s", feed.title));
}
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle("Select a feed")
.setItems(feedTitles.toArray(new String[feedTitles.size()]), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG, "Selected " + which);
if (which < folders.size()) {
selectedFolder = folders.get(which);
} else {
selectedFeed = feeds.get(which);
}
saveWidget();
}
});
builder.create().show();
}
private void saveWidget() {
if (selectedFeed == null && selectedFolder == null) {
toastError("Please select a feed");
return;
}
//update widget
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
RemoteViews rv = new RemoteViews(getPackageName(),
R.layout.newsblur_widget);
Intent intent = new Intent(this, BlurWidgetRemoteViewsService.class);
// Add the app widget ID to the intent extras.
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
String title;
if (selectedFeed != null) {
PrefsUtils.setWidgetFeed(this, appWidgetId, selectedFeed.feedId, selectedFeed.title);
title = selectedFeed.title;
} else {
PrefsUtils.setWidgetFolderName(this, appWidgetId, selectedFolder.name);
title = selectedFolder.name;
}
rv.setTextViewText(R.id.txt_feed_name, title);
rv.setRemoteAdapter(R.id.widget_list, intent);
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
Intent touchIntent = new Intent(this, NewsBlurWidgetProvider.class);
// Set the action for the intent.
// When the user touches a particular view, it will have the effect of
// broadcasting TOAST_ACTION.
touchIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(this, 0, touchIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate);
appWidgetManager.updateAppWidget(appWidgetId, rv);
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, resultValue);
finish();
}
}

View file

@ -6,67 +6,57 @@ import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
import com.newsblur.R;
import com.newsblur.activity.FeedReading;
import com.newsblur.activity.Reading;
import com.newsblur.database.BlurDatabaseHelper;
import com.newsblur.activity.AllStoriesItemsList;
import com.newsblur.activity.ItemsList;
import com.newsblur.activity.WidgetConfig;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.UIUtils;
public class NewsBlurWidgetProvider extends AppWidgetProvider {
public static String ACTION_OPEN_STORY = "ACTION_OPEN_STORY";
public static String EXTRA_ITEM_ID = "EXTRA_ITEM_ID";
public static String EXTRA_FEED_ID = "EXTRA_FEED_ID";
public class WidgetProvider extends AppWidgetProvider {
private static String TAG = "WidgetProvider";
private static String TAG = "NewsBlurWidgetProvider";
// Called when the BroadcastReceiver receives an Intent broadcast.
// Checks to see whether the intent's action is TOAST_ACTION. If it is, the app widget
// displays a Toast message for the current item.
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "onReceive");
AppWidgetManager mgr = AppWidgetManager.getInstance(context);
if (intent.getAction().equals(ACTION_OPEN_STORY)) {
int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
String storyHash = intent.getStringExtra(EXTRA_ITEM_ID);
String feedId = intent.getStringExtra(EXTRA_FEED_ID);
FeedSet fs = FeedSet.singleFeed(feedId);
Intent i = new Intent(context, FeedReading.class);
i.putExtra(Reading.EXTRA_FEEDSET, fs);
i.putExtra(Reading.EXTRA_STORY_HASH, storyHash);
if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_STORY)) {
String storyHash = intent.getStringExtra(WidgetUtils.EXTRA_ITEM_ID);
Intent i = new Intent(context, AllStoriesItemsList.class);
i.putExtra(ItemsList.EXTRA_FEED_SET, FeedSet.allFeeds());
i.putExtra(ItemsList.EXTRA_STORY_HASH, storyHash);
i.putExtra(ItemsList.EXTRA_WIDGET_STORY, true);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.getApplicationContext().startActivity(i);
} else if (intent.getAction().equals(WidgetUtils.ACTION_OPEN_CONFIG)) {
Intent i = new Intent(context, WidgetConfig.class);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.getApplicationContext().startActivity(i);
}
super.onReceive(context, intent);
}
/**
* Called to update at regular interval
* This is called to update the App Widget at intervals defined by the updatePeriodMillis attribute
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// update each of the app widgets with the remote adapter
Log.d(TAG, "onUpdate");
for (int i = 0; i < appWidgetIds.length; ++i) {
for (int appWidgetId : appWidgetIds) {
Log.d(TAG, "onUpdate iteration #" + i);
// Set up the intent that starts the BlurWidgetRemoteViewService, which will
// Set up the intent that starts the WidgetRemoteViewService, which will
// provide the views for this collection.
Intent intent = new Intent(context, BlurWidgetRemoteViewsService.class);
Intent intent = new Intent(context, WidgetRemoteViewsService.class);
// Add the app widget ID to the intent extras.
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
// Instantiate the RemoteViews object for the app widget layout.
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.newsblur_widget);
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.view_app_widget);
// Set up the RemoteViews object to use a RemoteViews adapter.
// This adapter connects
// to a RemoteViewsService through the specified intent.
@ -76,37 +66,33 @@ public class NewsBlurWidgetProvider extends AppWidgetProvider {
// The empty view is displayed when the collection has no items.
// It should be in the same layout used to instantiate the RemoteViews
// object above.
rv.setEmptyView(R.id.widget_list, R.id.empty_view);
rv.setEmptyView(R.id.widget_list, R.id.widget_empty_view);
rv.setTextViewText(R.id.txt_feed_name,
PrefsUtils.getWidgetFeedName(context, appWidgetIds[i]));
Intent configIntent = new Intent(context, WidgetProvider.class);
configIntent.setAction(WidgetUtils.ACTION_OPEN_CONFIG);
PendingIntent configIntentTemplate = PendingIntent.getBroadcast(context, WidgetUtils.RC_WIDGET_CONFIG, configIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.widget_empty_view, configIntentTemplate);
//
// Do additional processing specific to this app widget...
//
// This section makes it possible for items to have individualized behavior.
// It does this by setting up a pending intent template. Individuals items of a collection
// cannot set up their own pending intents. Instead, the collection as a whole sets
// up a pending intent template, and the individual items set a fillInIntent
// to create unique behavior on an item-by-item basis.
Intent touchIntent = new Intent(context, NewsBlurWidgetProvider.class);
Intent touchIntent = new Intent(context, WidgetProvider.class);
// Set the action for the intent.
// When the user touches a particular view, it will have the effect of
// broadcasting TOAST_ACTION.
touchIntent.setAction(NewsBlurWidgetProvider.ACTION_OPEN_STORY);
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
// broadcasting ACTION_OPEN_STORY.
touchIntent.setAction(WidgetUtils.ACTION_OPEN_STORY);
touchIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(context, 0, touchIntent,
PendingIntent touchIntentTemplate = PendingIntent.getBroadcast(context, WidgetUtils.RC_WIDGET_STORY, touchIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
rv.setPendingIntentTemplate(R.id.widget_list, touchIntentTemplate);
appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
appWidgetManager.updateAppWidget(appWidgetId, rv);
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
}
}
}

View file

@ -0,0 +1,15 @@
package com.newsblur.widget;
import android.support.annotation.ColorInt;
import android.widget.RemoteViews;
class WidgetRemoteViews extends RemoteViews {
WidgetRemoteViews(String packageName, int layoutId) {
super(packageName, layoutId);
}
void setViewBackgroundColor(int viewId, @ColorInt int color) {
setInt(viewId, "setBackgroundColor", color);
}
}

View file

@ -0,0 +1,269 @@
package com.newsblur.widget;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.View;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;
import com.newsblur.R;
import com.newsblur.domain.Feed;
import com.newsblur.domain.Story;
import com.newsblur.network.APIManager;
import com.newsblur.network.domain.StoriesResponse;
import com.newsblur.util.FeedSet;
import com.newsblur.util.FeedUtils;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
import com.newsblur.util.ReadFilter;
import com.newsblur.util.StoryOrder;
import com.newsblur.util.StoryUtils;
import com.newsblur.util.ThumbnailStyle;
import com.newsblur.util.UIUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
public class WidgetRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private static String TAG = "WidgetRemoteViewsFactory";
private Context context;
private APIManager apiManager;
private List<Story> storyItems = new ArrayList<>();
private FeedSet fs;
private int appWidgetId;
private boolean dataCompleted;
WidgetRemoteViewsFactory(Context context, Intent intent) {
com.newsblur.util.Log.d(TAG, "Constructor");
this.context = context;
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
/**
* The system calls onCreate() when creating your factory for the first time.
* This is where you set up any connections and/or cursors to your data source.
* <p>
* Heavy lifting,
* for example downloading or creating content etc, should be deferred to onDataSetChanged()
* or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
*/
@Override
public void onCreate() {
Log.d(TAG, "onCreate");
this.apiManager = new APIManager(context);
// widget could be created before app init
// wait for the dbHelper to be ready for use
while (FeedUtils.dbHelper == null) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (FeedUtils.dbHelper == null) {
FeedUtils.offerInitContext(context);
}
}
WidgetUtils.enableWidgetUpdate(context);
}
/**
* Allowed to run synchronous calls
*/
@Override
public RemoteViews getViewAt(int position) {
com.newsblur.util.Log.d(TAG, "getViewAt " + position);
Story story = storyItems.get(position);
WidgetRemoteViews rv = new WidgetRemoteViews(context.getPackageName(), R.layout.view_widget_story_item);
rv.setTextViewText(R.id.story_item_title, story.title);
rv.setTextViewText(R.id.story_item_content, story.shortContent);
rv.setTextViewText(R.id.story_item_author, story.authors);
rv.setTextViewText(R.id.story_item_feedtitle, story.extern_feedTitle);
CharSequence time = StoryUtils.formatShortDate(context, story.timestamp);
rv.setTextViewText(R.id.story_item_date, time);
// image dimensions same as R.layout.view_widget_story_item
FeedUtils.iconLoader.displayWidgetImage(story.extern_faviconUrl, R.id.story_item_feedicon, UIUtils.dp2px(context, 19), rv);
if (PrefsUtils.getThumbnailStyle(context) != ThumbnailStyle.OFF && !TextUtils.isEmpty(story.thumbnailUrl)) {
FeedUtils.thumbnailLoader.displayWidgetImage(story.thumbnailUrl, R.id.story_item_thumbnail, UIUtils.dp2px(context, 64), rv);
} else {
rv.setViewVisibility(R.id.story_item_thumbnail, View.GONE);
}
rv.setViewBackgroundColor(R.id.story_item_favicon_borderbar_1, UIUtils.decodeColourValue(story.extern_feedColor, Color.GRAY));
rv.setViewBackgroundColor(R.id.story_item_favicon_borderbar_2, UIUtils.decodeColourValue(story.extern_feedFade, Color.LTGRAY));
// set fill-intent which is used to fill in the pending intent template
// set on the collection view in WidgetProvider
Bundle extras = new Bundle();
extras.putString(WidgetUtils.EXTRA_ITEM_ID, story.storyHash);
Intent fillInIntent = new Intent();
fillInIntent.putExtras(extras);
rv.setOnClickFillInIntent(R.id.view_widget_item, fillInIntent);
return rv;
}
/**
* This allows for the use of a custom loading view which appears between the time that
* {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
* view will be used.
*
* @return The RemoteViews representing the desired loading view.
*/
@Override
public RemoteViews getLoadingView() {
return null;
}
/**
* @return The number of types of Views that will be returned by this factory.
*/
@Override
public int getViewTypeCount() {
return 1;
}
/**
* @param position The position of the item within the data set whose row id we want.
* @return The id of the item at the specified position.
*/
@Override
public long getItemId(int position) {
return storyItems.get(position).hashCode();
}
/**
* @return True if the same id always refers to the same object.
*/
@Override
public boolean hasStableIds() {
return true;
}
@Override
public void onDataSetChanged() {
com.newsblur.util.Log.d(TAG, "onDataSetChanged");
// if user logged out don't try to update widget
if (!WidgetUtils.isLoggedIn(context)) {
com.newsblur.util.Log.d(TAG, "onDataSetChanged - not logged in");
return;
}
if (dataCompleted) {
// we have all the stories data, just let the widget redraw
com.newsblur.util.Log.d(TAG, "onDataSetChanged - redraw widget");
dataCompleted = false;
} else {
setFeedSet();
if (fs == null) {
com.newsblur.util.Log.d(TAG, "onDataSetChanged - null feed set. Show empty view");
setStories(new Story[]{}, new HashMap<String, Feed>(0));
return;
}
com.newsblur.util.Log.d(TAG, "onDataSetChanged - fetch stories");
StoriesResponse response = apiManager.getStories(fs, 1, StoryOrder.NEWEST, ReadFilter.ALL);
if (response == null || response.stories == null) {
com.newsblur.util.Log.d(TAG, "Error fetching widget stories");
} else {
com.newsblur.util.Log.d(TAG, "Fetched widget stories");
processStories(response.stories);
}
}
}
/**
* Called when the last RemoteViewsAdapter that is associated with this factory is
* unbound.
*/
@Override
public void onDestroy() {
com.newsblur.util.Log.d(TAG, "onDestroy");
WidgetUtils.disableWidgetUpdate(context);
PrefsUtils.removeWidgetData(context);
}
/**
* @return Count of items.
*/
@Override
public int getCount() {
return Math.min(storyItems.size(), WidgetUtils.STORIES_LIMIT);
}
private void processStories(final Story[] stories) {
com.newsblur.util.Log.d(TAG, "processStories");
final HashMap<String, Feed> feedMap = new HashMap<>();
Loader<Cursor> loader = FeedUtils.dbHelper.getFeedsLoader();
loader.registerListener(loader.getId(), new Loader.OnLoadCompleteListener<Cursor>() {
@Override
public void onLoadComplete(@NonNull Loader<Cursor> loader, @Nullable Cursor cursor) {
while (cursor != null && cursor.moveToNext()) {
Feed feed = Feed.fromCursor(cursor);
if (feed.active) {
feedMap.put(feed.feedId, feed);
}
}
setStories(stories, feedMap);
}
});
loader.startLoading();
}
private void setStories(Story[] stories, HashMap<String, Feed> feedMap) {
com.newsblur.util.Log.d(TAG, "setStories");
for (Story story : stories) {
Feed storyFeed = feedMap.get(story.feedId);
if (storyFeed != null) {
bindStoryValues(story, storyFeed);
}
}
this.storyItems.clear();
this.storyItems.addAll(Arrays.asList(stories));
// we have the data, notify data set changed
dataCompleted = true;
invalidate();
}
private void bindStoryValues(Story story, Feed feed) {
story.thumbnailUrl = Story.guessStoryThumbnailURL(story);
story.extern_faviconBorderColor = feed.faviconBorder;
story.extern_faviconUrl = feed.faviconUrl;
story.extern_feedTitle = feed.title;
story.extern_feedFade = feed.faviconFade;
story.extern_feedColor = feed.faviconColor;
}
private void invalidate() {
com.newsblur.util.Log.d(TAG, "Invalidate app widget with id: " + appWidgetId);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_list);
}
private void setFeedSet() {
Set<String> feedIds = PrefsUtils.getWidgetFeedIds(context);
if (feedIds == null || !feedIds.isEmpty()) {
fs = FeedSet.widgetFeeds(feedIds);
} else {
// no feeds selected. Widget will show tap to config view
fs = null;
}
}
}

View file

@ -0,0 +1,17 @@
package com.newsblur.widget;
import android.content.Intent;
import android.widget.RemoteViewsService;
import com.newsblur.util.Log;
public class WidgetRemoteViewsService extends RemoteViewsService {
private static String TAG = "WidgetRemoteViewsFactory";
@Override
public RemoteViewsService.RemoteViewsFactory onGetViewFactory(Intent intent) {
Log.d(TAG, "onGetViewFactory");
return new WidgetRemoteViewsFactory(this.getApplicationContext(), intent);
}
}

View file

@ -0,0 +1,25 @@
package com.newsblur.widget;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.Nullable;
import com.newsblur.R;
import com.newsblur.util.Log;
public class WidgetUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, @Nullable Intent intent) {
if (intent != null && intent.getAction() != null &&
intent.getAction().equals(WidgetUtils.ACTION_UPDATE_WIDGET)) {
Log.d(this.getClass().getName(), "Received " + WidgetUtils.ACTION_UPDATE_WIDGET);
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
widgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
}
}

View file

@ -0,0 +1,74 @@
package com.newsblur.widget;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.SystemClock;
import com.newsblur.R;
import com.newsblur.util.Log;
import com.newsblur.util.PrefsUtils;
public class WidgetUtils {
private static String TAG = "WidgetUtils";
public static String ACTION_UPDATE_WIDGET = "ACTION_UPDATE_WIDGET";
public static String ACTION_OPEN_STORY = "ACTION_OPEN_STORY";
public static String ACTION_OPEN_CONFIG = "ACTION_OPEN_CONFIG";
public static String EXTRA_ITEM_ID = "EXTRA_ITEM_ID";
public static int RC_WIDGET_UPDATE = 1;
public static int RC_WIDGET_STORY = 2;
public static int RC_WIDGET_CONFIG = 3;
public static int STORIES_LIMIT = 5;
static void enableWidgetUpdate(Context context) {
Log.d(TAG, "enableWidgetUpdate");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Intent intent = getUpdateIntent(context);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, RC_WIDGET_UPDATE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
int widgetUpdateInterval = 1000 * 60 * 5;
long startAlarmAt = SystemClock.currentThreadTimeMillis() + widgetUpdateInterval;
alarmManager.setInexactRepeating(AlarmManager.RTC, startAlarmAt, widgetUpdateInterval, pendingIntent);
}
public static void disableWidgetUpdate(Context context) {
Log.d(TAG, "disableWidgetUpdate");
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, RC_WIDGET_UPDATE, getUpdateIntent(context), PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.cancel(pendingIntent);
}
public static void resetWidgetUpdate(Context context) {
if (hasActiveAppWidgets(context)) {
WidgetUtils.enableWidgetUpdate(context);
}
}
public static boolean hasActiveAppWidgets(Context context) {
AppWidgetManager widgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = widgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
return appWidgetIds.length > 0;
}
public static void notifyViewDataChanged(Context context) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
}
public static boolean isLoggedIn(Context context) {
return PrefsUtils.getUniqueLoginKey(context) != null;
}
private static Intent getUpdateIntent(Context context) {
Intent intent = new Intent(context, WidgetUpdateReceiver.class);
intent.setAction(ACTION_UPDATE_WIDGET);
return intent;
}
}

View file

@ -270,10 +270,10 @@
#pragma mark - Table view data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return 12;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
return 13;
} else {
return 11;
return 12;
}
}
@ -281,7 +281,7 @@
static NSString *CellIndentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIndentifier];
NSUInteger iPadOffset = [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone ? 0 : 1;
NSUInteger iPadOffset = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone ? 0 : 1;
if (indexPath.row == 7) {
return [self makeFontSizeTableCell];
@ -396,7 +396,7 @@
}
// if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
// if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
// if (indexPath.row != 2 && indexPath.row != 3) {
// // if we're opening another popover, then don't animate out - it looks strange
// [self.appDelegate.masterContainerViewController hidePopover];

View file

@ -55,7 +55,6 @@
@property (readwrite) BOOL isHidingStory;
@property (readwrite) BOOL feedDetailIsVisible;
@property (readwrite) BOOL keyboardIsShown;
@property (nonatomic) UIBackgroundTaskIdentifier reorientBackgroundTask;
@end
@ -230,11 +229,9 @@
if (self.feedDetailIsVisible) {
// Defer this in the background, to avoid misaligning the detail views
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
self.reorientBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:self.reorientBackgroundTask];
self.reorientBackgroundTask = UIBackgroundTaskInvalid;
}];
[self performSelector:@selector(delayedReorientPages) withObject:nil afterDelay:0.5];
NSString *networkOperationIdentifier = [appDelegate beginNetworkOperation];
[self performSelector:@selector(delayedReorientPages:) withObject:networkOperationIdentifier afterDelay:0.5];
} else {
[self.storyPageControl reorientPages];
}
@ -268,10 +265,9 @@
}
}
- (void)delayedReorientPages {
- (void)delayedReorientPages:(NSString *)identifier {
[self.storyPageControl reorientPages];
[[UIApplication sharedApplication] endBackgroundTask:self.reorientBackgroundTask];
self.reorientBackgroundTask = UIBackgroundTaskInvalid;
[appDelegate endNetworkOperation:identifier];
}
- (void)checkSize:(CGSize)size {

View file

@ -342,6 +342,8 @@ SFSafariViewControllerDelegate> {
- (void)recalculateIntelligenceScores:(id)feedId;
- (void)cancelRequests;
- (NSString *)beginNetworkOperation;
- (void)endNetworkOperation:(NSString *)networkOperationIdentifier;
- (void)GET:(NSString *)urlString parameters:(id)parameters
success:(void (^)(NSURLSessionDataTask *, id))success

View file

@ -3314,7 +3314,7 @@
[gradientView addSubview:titleImageView];
} else {
gradientView = [NewsBlurAppDelegate
makeGradientView:CGRectMake(0, -1, rect.size.width, 10)
makeGradientView:CGRectMake(0, rect.origin.y, rect.size.width, 10)
// hard coding the 1024 as a hack for window.frame.size.width
startColor:[feed objectForKey:@"favicon_fade"]
endColor:[feed objectForKey:@"favicon_color"]

View file

@ -253,7 +253,7 @@ static NSArray<NSString *> *NewsBlurTopSectionNames;
// [self.feedTitlesTable selectRowAtIndexPath:self.currentRowAtIndexPath
// animated:NO
// scrollPosition:UITableViewScrollPositionNone];
[self.notifier setNeedsLayout];
[self hideNotifier];
}
if (self.searchFeedIds) {

View file

@ -125,7 +125,7 @@
doubleDoubleTapGesture.delegate = self;
[self.webView addGestureRecognizer:doubleDoubleTapGesture];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if ([UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
UIPinchGestureRecognizer *pinchGesture = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:@selector(pinchGesture:)];
[self.webView addGestureRecognizer:pinchGesture];
@ -369,7 +369,7 @@
}
- (BOOL)isPhoneOrCompact {
return [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.appDelegate.isCompactWidth;
return [UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone || self.appDelegate.isCompactWidth;
}
// allow keyboard comands
@ -607,6 +607,11 @@
}
- (void)drawFeedGradient {
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
BOOL shouldOffsetFeedGradient = UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone && !UIInterfaceOrientationIsLandscape(orientation) && self.navigationController.navigationBarHidden && !shouldHideStatusBar;
CGFloat yOffset = shouldOffsetFeedGradient ? appDelegate.storyPageControl.statusBarBackgroundView.bounds.size.height - 1 : -1;
NSString *feedIdStr = [NSString stringWithFormat:@"%@",
[self.activeStory
objectForKey:@"story_feed_id"]];
@ -619,7 +624,7 @@
self.feedTitleGradient = [appDelegate
makeFeedTitleGradient:feed
withRect:CGRectMake(0, -1, CGRectGetWidth(self.view.bounds), 21)]; // 1024 hack for self.webView.frame.size.width
withRect:CGRectMake(0, yOffset, CGRectGetWidth(self.view.bounds), 21)]; // 1024 hack for self.webView.frame.size.width
self.feedTitleGradient.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.feedTitleGradient.tag = FEED_TITLE_GRADIENT_TAG; // Not attached yet. Remove old gradients, first.
@ -640,7 +645,7 @@
[self.webView insertSubview:feedTitleGradient aboveSubview:self.webView.scrollView];
if (@available(iOS 11.0, *)) {
if (self.view.safeAreaInsets.top > 0.0 && [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
if (self.view.safeAreaInsets.top > 0.0 && [UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone && shouldHideStatusBar) {
feedTitleGradient.alpha = self.navigationController.navigationBarHidden ? 1 : 0;
[UIView animateWithDuration:0.3 animations:^{
@ -1409,7 +1414,11 @@
// appDelegate.storyPageControl.traverseView.frame = CGRectMake(tvf.origin.x,
// (webpageHeight - topPosition) - tvf.size.height - safeBottomMargin,
// tvf.size.width, tvf.size.height);
appDelegate.storyPageControl.traverseBottomConstraint.constant = viewportHeight - (webpageHeight - topPosition) + safeBottomMargin;
if (webpageHeight > 0) {
appDelegate.storyPageControl.traverseBottomConstraint.constant = viewportHeight - (webpageHeight - topPosition) + safeBottomMargin;
} else {
appDelegate.storyPageControl.traverseBottomConstraint.constant = safeBottomMargin;
}
// appDelegate.storyPageControl.traverseBottomConstraint.constant = safeBottomMargin;
}
} else if (!singlePage && (atTop && !atBottom)) {
@ -1896,7 +1905,7 @@
}
- (BOOL)canHideNavigationBar {
if ([[UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone || self.presentedViewController != nil) {
if ([UIDevice currentDevice] userInterfaceIdiom] != UIUserInterfaceIdiomPhone || self.presentedViewController != nil) {
return NO;
}

View file

@ -228,15 +228,15 @@
[self.view addConstraint:self.notifier.topOffsetConstraint];
[self.notifier hideNow];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.traverseBottomConstraint.constant = 50;
if ([UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
self.navigationItem.rightBarButtonItems = [NSArray arrayWithObjects:
originalStoryButton,
separatorBarButton,
fontSettingsButton, nil];
}
[self updateTheme];
[self.scrollView addObserver:self forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:nil];
@ -266,6 +266,8 @@
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateTheme];
[self updateAutoscrollButtons];
[self updateTraverseBackground];
[self setNextPreviousButtons];
@ -500,7 +502,7 @@
NSUserDefaults *userPreferences = [NSUserDefaults standardUserDefaults];
BOOL swipeEnabled = [[userPreferences stringForKey:@"story_detail_swipe_left_edge"]
isEqualToString:@"pop_to_story_list"];;
isEqualToString:@"pop_to_story_list"];
self.navigationController.interactivePopGestureRecognizer.enabled = swipeEnabled;
if (hide) {
@ -1239,7 +1241,10 @@
}
self.scrollView.scrollsToTop = NO;
NSInteger topPosition = currentPage.webView.scrollView.contentOffset.y;
NSUserDefaults *preferences = [NSUserDefaults standardUserDefaults];
BOOL shouldHideStatusBar = [preferences boolForKey:@"story_hide_status_bar"];
NSInteger statusBarOffset = shouldHideStatusBar ? 0 : self.statusBarHeight;
NSInteger topPosition = currentPage.webView.scrollView.contentOffset.y + statusBarOffset;
BOOL canHide = currentPage.canHideNavigationBar && topPosition >= 0;
if (!canHide && self.isHorizontal && self.navigationController.navigationBarHidden) {

View file

@ -2797,7 +2797,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1120;
LastUpgradeCheck = 1140;
LastUpgradeCheck = 1150;
ORGANIZATIONNAME = NewsBlur;
TargetAttributes = {
1749390F1C251BFE003D98AA = {

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1150"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1150"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1150"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1140"
LastUpgradeVersion = "1150"
wasCreatedForAppExtension = "YES"
version = "1.3">
<BuildAction

View file

@ -11,6 +11,8 @@
@interface ShareViewController () <NSURLSessionDelegate>
@property (nonatomic, strong) NSString *itemTitle;
@end
@implementation ShareViewController
@ -26,6 +28,8 @@
}
- (void)didSelectPost {
self.itemTitle = nil;
NSItemProvider *itemProvider = [self providerWithURL];
NSLog(@"ShareExt: didSelectPost");
@ -55,6 +59,12 @@
for (NSExtensionItem *extensionItem in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in extensionItem.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {
self.itemTitle = extensionItem.attributedTitle.string;
if (!self.itemTitle.length) {
self.itemTitle = extensionItem.attributedContentText.string;
}
return itemProvider;
}
}
@ -79,14 +89,20 @@
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.newsblur.NewsBlur-Group"];
NSString *host = [defaults objectForKey:@"share:host"];
NSString *token = [defaults objectForKey:@"share:token"];
NSString *title = self.itemTitle;
NSString *comments = self.contentText;
if (title && [comments isEqualToString:title]) {
comments = @"";
}
if (text && [comments isEqualToString:text]) {
comments = @"";
}
NSCharacterSet *characterSet = [NSCharacterSet URLQueryAllowedCharacterSet];
NSString *encodedURL = url ? [url.absoluteString stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
NSString *encodedTitle = title ? [title stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
NSString *encodedContent = text ? [text stringByAddingPercentEncodingWithAllowedCharacters:characterSet] : @"";
NSString *encodedComments = [comments stringByAddingPercentEncodingWithAllowedCharacters:characterSet];
// NSInteger time = [[NSDate date] timeIntervalSince1970];
@ -96,7 +112,7 @@
NSURL *requestURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/api/share_story/%@", host, token]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
request.HTTPMethod = @"POST";
NSString *postBody = [NSString stringWithFormat:@"story_url=%@&title=&content=%@&comments=%@", encodedURL, encodedContent, encodedComments];
NSString *postBody = [NSString stringWithFormat:@"story_url=%@&title=%@&content=%@&comments=%@", encodedURL, encodedTitle, encodedContent, encodedComments];
request.HTTPBody = [postBody dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionTask *myTask = [mySession dataTaskWithRequest:request];
[myTask resume];

View file

@ -37,7 +37,7 @@ frontend public
option http-server-close
# Redirect all HTTP traffic to HTTPS
# redirect scheme https code 301 if !{ ssl_fc }
redirect scheme https code 301 if !{ ssl_fc }
acl gunicorn_dead nbsrv(gunicorn) lt 1
acl nginx_dead nbsrv(nginx) lt 1

51
fabfile.py vendored
View file

@ -212,7 +212,6 @@ def setup_common():
# setup_pymongo_repo()
setup_logrotate()
setup_nginx()
# setup_imaging()
setup_munin()
def setup_all():
@ -458,25 +457,6 @@ def setup_libxml_code():
def setup_psycopg():
sudo('easy_install -U psycopg2')
# def setup_python():
# # sudo('easy_install -U $(<%s)' %
# # os.path.join(env.NEWSBLUR_PATH, 'config/requirements.txt'))
# pip()
# put('config/pystartup.py', '.pystartup')
#
# # with cd(os.path.join(env.NEWSBLUR_PATH, 'vendor/cjson')):
# # sudo('python setup.py install')
#
# with settings(warn_only=True):
# sudo('echo "import sys; sys.setdefaultencoding(\'utf-8\')" | sudo tee /usr/lib/python2.7/sitecustomize.py')
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/httplib2-0.8-py2.7.egg/EGG-INFO/top_level.txt")
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/python_dateutil-2.1-py2.7.egg/EGG-INFO/top_level.txt")
# sudo("chmod a+r /usr/local/lib/python2.7/dist-packages/httplib2-0.8-py2.7.egg/httplib2/cacerts.txt")
#
# if env.user == 'ubuntu':
# with settings(warn_only=True):
# sudo('chown -R ubuntu.ubuntu /home/ubuntu/.python-eggs')
def setup_virtualenv():
sudo('rm -fr ~/.cache') # Clean `sudo pip`
sudo('pip install --upgrade virtualenv')
@ -529,10 +509,6 @@ def solo_pip(role):
pip()
celery()
# PIL - Only if python-imaging didn't install through apt-get, like on Mac OS X.
def setup_imaging():
sudo('easy_install --always-unzip pil')
def setup_supervisor():
sudo('apt-get -y install supervisor')
put('config/supervisord.conf', '/etc/supervisor/supervisord.conf', use_sudo=True)
@ -820,11 +796,11 @@ def assemble_certificates():
local('cat STAR_newsblur_com.crt EssentialSSLCA_2.crt ComodoUTNSGCCA.crt UTNAddTrustSGCCA.crt AddTrustExternalCARoot.crt > newsblur.com.crt')
def copy_certificates():
cert_path = '%s/config/certificates' % env.NEWSBLUR_PATH
cert_path = os.path.join(env.NEWSBLUR_PATH, 'config/certificates')
run('mkdir -p %s' % cert_path)
put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.crt'), cert_path)
put(os.path.join(env.SECRETS_PATH, 'certificates/newsblur.com.key'), cert_path)
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/newsblur.com.pem'), cert_path)
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/newsblur.com.crt'), os.path.join(cert_path, 'newsblur.com.pem')) # For backwards compatibility with hard-coded nginx configs
put(os.path.join(env.SECRETS_PATH, 'certificates/comodo/dhparams.pem'), cert_path)
put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps_development.pem'), cert_path)
# openssl x509 -in aps.cer -inform DER -outform PEM -out aps.pem
@ -832,10 +808,21 @@ def copy_certificates():
# Export aps.p12 from aps.cer using Keychain Assistant
# openssl pkcs12 -in aps.p12 -out aps.p12.pem -nodes
put(os.path.join(env.SECRETS_PATH, 'certificates/ios/aps.p12.pem'), cert_path)
run('cat %s/newsblur.com.pem > %s/newsblur.pem' % (cert_path, cert_path))
run('cat %s/newsblur.com.crt > %s/newsblur.pem' % (cert_path, cert_path))
run('echo "\n" >> %s/newsblur.pem' % (cert_path))
run('cat %s/newsblur.com.key >> %s/newsblur.pem' % (cert_path, cert_path))
def setup_certbot():
sudo('add-apt-repository -y universe')
sudo('add-apt-repository -y ppa:certbot/certbot')
sudo('apt-get update')
sudo('apt-get install -y certbot')
sudo('apt-get install -y python3-certbot-dns-dnsimple')
run('echo "dns_dnsimple_token = %s" > dnsimple.ini')
run('chmod 0400 dnsimple.ini')
sudo('certbot certonly -n --agree-tos --email samuel@newsblur.com --domains "*.newsblur.com" --dns-dnsimple --dns-dnsimple-credentials %s' % (settings.DNSIMPLE_TOKEN))
run('rm dnsimple.ini')
@parallel
def maintenance_on():
role = role_for_host()
@ -1392,6 +1379,16 @@ def setup_usage_monitor():
sudo('ln -fs %s/utils/monitor_disk_usage.py /etc/cron.daily/monitor_disk_usage' % env.NEWSBLUR_PATH)
sudo('/etc/cron.daily/monitor_disk_usage')
@parallel
def setup_feeds_fetched_monitor():
sudo('ln -fs %s/utils/monitor_task_fetches.py /etc/cron.hourly/monitor_task_fetches' % env.NEWSBLUR_PATH)
sudo('/etc/cron.hourly/monitor_task_fetches')
@parallel
def setup_newsletter_monitor():
sudo('ln -fs %s/utils/monitor_newsletter_delivery.py /etc/cron.hourly/monitor_newsletter_delivery' % env.NEWSBLUR_PATH)
sudo('/etc/cron.hourly/monitor_newsletter_delivery')
@parallel
def setup_redis_monitor():
run('sleep 5') # Wait for redis to startup so the log file is there

View file

@ -135,3 +135,26 @@ if len(logging._handlerList) < 1:
datefmt='%b %d %H:%M:%S',
handler=logging.StreamHandler)
S3_ACCESS_KEY = '000000000000000000000'
S3_SECRET = '000000000000000000000000/0000000000000000'
S3_BACKUP_BUCKET = 'newsblur_backups'
S3_PAGES_BUCKET_NAME = 'pages-dev.newsblur.com'
S3_ICONS_BUCKET_NAME = 'icons-dev.newsblur.com'
S3_AVATARS_BUCKET_NAME = 'avatars-dev.newsblur.com'
MAILGUN_ACCESS_KEY = 'key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
MAILGUN_SERVER_NAME = 'newsblur.com'
DO_TOKEN_LOG = '0000000000000000000000000000000000000000000000000000000000000000'
DO_TOKEN_FABRIC = '0000000000000000000000000000000000000000000000000000000000000000'
SERVER_NAME = "nblocalhost"
NEWSBLUR_URL = 'http://nb.local.com'
SESSION_ENGINE = 'redis_sessions.session'
# CORS_ORIGIN_REGEX_WHITELIST = ('^(https?://)?(\w+\.)?nb.local\.com$', )
YOUTUBE_API_KEY = "000000000000000000000000000000000000000"
RECAPTCHA_SECRET_KEY = "0000000000000000000000000000000000000000"
IMAGES_SECRET_KEY = "0000000000000000000000000000000"

View file

@ -1537,12 +1537,16 @@
this.flags['loaded_next_after_load'] = true;
var next = $.getQueryString('next') || $.getQueryString('test');
var add_url = $.getQueryString('add') || $.getQueryString('url');
var story = $.getQueryString('story');
if (next == 'notifications') {
_.defer(function() {
NEWSBLUR.reader.open_notifications_modal(NEWSBLUR.assets.active_feed && NEWSBLUR.assets.active_feed.id);
});
}
if (add_url) {
NEWSBLUR.reader.open_add_feed_modal({url: url});
}
if (story) {
this.flags['select_story_in_feed'] = story;
}
@ -3180,7 +3184,11 @@
clearInterval(this.flags['bouncing_callout']);
$.modal.close();
NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options);
if (NEWSBLUR.Globals.is_anonymous && NEWSBLUR.welcome) {
NEWSBLUR.welcome.show_signin_form();
} else {
NEWSBLUR.add_feed = NEWSBLUR.ReaderAddFeed.create(options);
}
},
open_manage_feed_modal: function(feed_id) {
@ -6726,6 +6734,12 @@
self.reload_feed();
}
});
$document.bind('keydown', 'shift+r', function(e) {
e.preventDefault();
if (self.active_feed) {
self.force_instafetch_stories();
}
});
$document.bind('keydown', 'enter', function(e) {
e.preventDefault();
if (self.flags['feed_view_showing_story_view']) {

View file

@ -305,8 +305,10 @@ NEWSBLUR.Views.FeedList = Backbone.View.extend({
var url = $.getQueryString('url') || $.getQueryString('add');
if (url) {
NEWSBLUR.reader.open_add_feed_modal({url: url});
route_found = true;
}
// This removes the query string from the URL.
if (!route_found && window.history.replaceState && !$.getQueryString('test')) {
// In case this needs to be found again: window.location.href = BACKBONE
window.history.replaceState({}, null, '/');

View file

@ -103,6 +103,11 @@ NEWSBLUR.Welcome = Backbone.View.extend({
this.flags.on_header_caption = true;
this.flags.on_signin = true;
var add_url = $.getQueryString('add') || $.getQueryString('url');
if (add_url) {
this.$("input[name=next]").val("/?add=" + add_url);
}
this.$el.scrollTo(0, 500, {queue: false, easing: 'easeInOutQuint'});
_.delay(_.bind(function() {

View file

@ -4,7 +4,7 @@
"description": "Servers used in running NewsBlur",
"main": "favicons.js",
"dependencies": {
"@postlight/mercury-parser": "^2.1.1",
"@postlight/mercury-parser": "^2.2.0",
"connect-busboy": "^0.0.2",
"esm": "^3.2.22",
"express": "^4.16.4",

View file

@ -51,6 +51,10 @@ NEWSBLUR_URL = 'http://www.newsblur.com'
IMAGES_URL = 'https://imageproxy.newsblur.com'
SECRET_KEY = 'YOUR_SECRET_KEY'
IMAGES_SECRET_KEY = "YOUR_SECRET_IMAGE_KEY"
DNSIMPLE_TOKEN = "YOUR_DNSIMPLE_TOKEN"
RECAPTCHA_SECRET_KEY = "YOUR_RECAPTCHA_KEY"
YOUTUBE_API_KEY = "YOUR_YOUTUBE_API_KEY"
IMAGES_SECRET_KEY = "YOUR_IMAGES_SECRET_KEY"
# ===================
# = Global Settings =

View file

@ -88,6 +88,8 @@
{{ login_form.password.label_tag }}
{{ login_form.password }}
</div>
{{ login_form.add }}
<input name="submit" type="submit" class="NB-modal-submit-button NB-modal-submit-green" value="log in" />
<input type="hidden" name="next" value="/" />
</form>

View file

@ -1122,6 +1122,38 @@
desc: "Object with folders and feed ids."
required: true
example: "[12, 24, 36, <br>{'Blogs': [56, 67, 78,<br />{'Photoblogs': [42]}]}]"
- url: /reader/save_search
method: POST
short_desc: "Add a new saved search feed."
long_desc:
- "Add a new saved search feed, which is a shortcut to a search query on a specific feed."
params:
- key: feed_id
desc: "A single feed id of feed to search."
required: true
example: "42"
- key: query
desc: >
Search for a keyword or phrase in the feed. Note that only premium users can search feeds.
required: true
example: "pizza"
- url: /reader/delete_search
method: POST
short_desc: "Removes a saved search feed."
long_desc:
- "Deletes a saved search feed."
params:
- key: feed_id
desc: "A single feed id of feed to search."
required: true
example: "42"
- key: query
desc: >
The query originally used to save the search.
required: true
example: "pizza"
- "intelligence classifiers":

View file

@ -100,7 +100,7 @@ class FetchFeed:
(self.feed.log_title[:30], address))
return FEED_ERRHTTP, None
self.fpf = feedparser.parse(youtube_feed)
elif re.match(r'(https?)?://twitter.com/\w+/?$', qurl(address, remove=['_'])):
elif re.match(r'(https?)?://twitter.com/\w+/?', qurl(address, remove=['_'])):
twitter_feed = self.fetch_twitter(address)
if not twitter_feed:
logging.debug(u' ***> [%-30s] ~FRTwitter fetch failed: %s' %

View file

@ -14,15 +14,17 @@ def main():
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
hostname = socket.gethostname()
percent = int(percent.strip('%'))
admin_email = settings.ADMINS[0][1]
if percent > 95:
requests.post(
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
auth=("api", settings.MAILGUN_ACCESS_KEY),
data={"from": "NewsBlur Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
"to": [settings.ADMINS[0][1]],
data={"from": "NewsBlur Disk Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
"to": [admin_email],
"subject": "%s hit %s%% disk usage!" % (hostname, percent),
"text": "Usage on %s: %s" % (hostname, output)})
print " ---> Disk usage is NOT fine: %s / %s%% used" % (hostname, percent)
else:
print " ---> Disk usage is fine: %s / %s%% used" % (hostname, percent)

View file

@ -0,0 +1,40 @@
#!/srv/newsblur/venv/newsblur/bin/python
import sys
sys.path.append('/srv/newsblur')
import subprocess
import requests
import settings
import socket
def main():
df = subprocess.Popen(["df", "/"], stdout=subprocess.PIPE)
output = df.communicate()[0]
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
hostname = socket.gethostname()
admin_email = settings.ADMINS[0][1]
r = requests.get("https://api.mailgun.net/v3/newsletters.newsblur.com/stats/total",
auth=("api", settings.MAILGUN_ACCESS_KEY),
params={"event": ["accepted", "delivered", "failed"],
"duration": "2h"})
stats = r.json()['stats'][0]
delivered = stats['delivered']['total']
accepted = stats['delivered']['total']
bounced = stats['failed']['permanent']['total'] + stats['failed']['temporary']['total']
if bounced / float(delivered) > 0.5:
requests.post(
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
auth=("api", settings.MAILGUN_ACCESS_KEY),
data={"from": "NewsBlur Newsletter Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
"to": [admin_email],
"subject": "%s newsletters bounced: %s > %s > %s" % (hostname, accepted, delivered, bounced),
"text": "Newsletters are not being delivered! %s delivered, %s bounced" % (delivered, bounced)})
print " ---> %s newsletters bounced: %s > %s > %s" % (hostname, accepted, delivered, bounced)
else:
print " ---> %s newsletters OK: %s > %s > %s" % (hostname, accepted, delivered, bounced)
if __name__ == '__main__':
main()

44
utils/monitor_task_fetches.py Executable file
View file

@ -0,0 +1,44 @@
#!/srv/newsblur/venv/newsblur/bin/python
import sys
sys.path.append('/srv/newsblur')
import subprocess
import requests
import settings
import socket
import pymongo
def main():
df = subprocess.Popen(["df", "/"], stdout=subprocess.PIPE)
output = df.communicate()[0]
device, size, used, available, percent, mountpoint = output.split("\n")[1].split()
hostname = socket.gethostname()
percent = int(percent.strip('%'))
admin_email = settings.ADMINS[0][1]
failed = False
feeds_fetched = 0
try:
client = pymongo.MongoClient('mongodb://%s' % settings.MONGO_DB['host'])
feeds_fetched = client.newsblur.statistics.find_one({"key": "feeds_fetched"})['value']
except Exception, e:
failed = e
if feeds_fetched < 5000000:
failed = True
if failed:
requests.post(
"https://api.mailgun.net/v2/%s/messages" % settings.MAILGUN_SERVER_NAME,
auth=("api", settings.MAILGUN_ACCESS_KEY),
data={"from": "NewsBlur Task Monitor: %s <admin@%s.newsblur.com>" % (hostname, hostname),
"to": [admin_email],
"subject": "%s feeds fetched falling: %s" % (hostname, feeds_fetched),
"text": "Feed fetches are falling (%s): %s" % (hostname, feeds_fetched, failed)})
print(" ---> Feeds fetched falling! %s" % (feeds_fetched))
else:
print(" ---> Feeds fetched OK: %s" % (feeds_fetched))
if __name__ == '__main__':
main()

View file

@ -18,25 +18,38 @@ class TwitterFetcher:
self.options = options or {}
def fetch(self, address=None):
data = {}
if not address:
address = self.feed.feed_address
self.address = address
twitter_user = None
username = self.extract_username()
if not username:
return
twitter_user = self.fetch_user(username)
if not twitter_user:
return
if '/lists/' in address:
list_id = self.extract_list_id()
if not list_id:
return
tweets = self.user_timeline(twitter_user)
tweets, list_info = self.fetch_list_timeline(list_id)
if not tweets:
return
data['title'] = "%s on Twitter" % list_info.full_name
data['link'] = "https://twitter.com%s" % list_info.uri
data['description'] = "%s on Twitter" % list_info.full_name
else:
username = self.extract_username()
if not username:
return
data = {}
data['title'] = "%s on Twitter" % username
data['link'] = "https://twitter.com/%s" % username
data['description'] = "%s on Twitter" % username
twitter_user = self.fetch_user(username)
if not twitter_user:
return
tweets = self.user_timeline(twitter_user)
data['title'] = "%s on Twitter" % username
data['link'] = "https://twitter.com/%s" % username
data['description'] = "%s on Twitter" % username
data['lastBuildDate'] = datetime.datetime.utcnow()
data['generator'] = 'NewsBlur Twitter API Decrapifier - %s' % settings.NEWSBLUR_URL
data['docs'] = None
@ -52,7 +65,7 @@ class TwitterFetcher:
def extract_username(self):
username = None
try:
username_groups = re.search('twitter.com/(\w+)/?', self.address)
username_groups = re.search('twitter.com/(\w+)/?$', self.address)
if not username_groups:
return
username = username_groups.group(1)
@ -60,8 +73,20 @@ class TwitterFetcher:
return
return username
def fetch_user(self, username):
def extract_list_id(self):
list_id = None
try:
list_groups = re.search('twitter.com/i/lists/(\w+)/?', self.address)
if not list_groups:
return
list_id = list_groups.group(1)
except IndexError:
return
return list_id
def twitter_api(self):
twitter_api = None
social_services = None
if self.options.get('requesting_user_id', None):
@ -97,6 +122,13 @@ class TwitterFetcher:
(self.feed.log_title[:30], self.address, usersubs[0].user.username))
return
return twitter_api
def fetch_user(self, username):
twitter_api = self.twitter_api()
if not twitter_api:
return
try:
twitter_user = twitter_api.get_user(username)
except TypeError, e:
@ -170,6 +202,89 @@ class TwitterFetcher:
if not tweets:
return []
return tweets
def fetch_list_timeline(self, list_id):
twitter_api = self.twitter_api()
if not twitter_api:
return
try:
list_timeline = twitter_api.list_timeline(list_id=list_id, tweet_mode='extended')
except TypeError, e:
logging.debug(u' ***> [%-30s] ~FRTwitter list fetch failed, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: %s" % (e))
return
except tweepy.error.TweepError, e:
message = str(e).lower()
if ((len(e.args) >= 2 and e.args[2] == 63) or
('temporarily locked' in message)):
# Suspended
logging.debug(u' ***> [%-30s] ~FRTwitter failed, user suspended, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: User suspended")
return
elif 'suspended' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter user suspended, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: User suspended")
return
elif 'expired token' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter user expired, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: Expired token")
social_services.disconnect_twitter()
return
elif 'not found' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: User not found")
return
elif 'over capacity' in message or 'Max retries' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter over capacity, ignoring... %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(460, "Twitter Error: Over capacity")
return
else:
raise e
list_info = twitter_api.get_list(list_id=list_id)
if not list_timeline:
return [], list_info
return list_timeline, list_info
def tweets_from_list(self, list_timeline):
try:
tweets = twitter_user.timeline(tweet_mode='extended')
except tweepy.error.TweepError, e:
message = str(e).lower()
if 'not authorized' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter timeline failed, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: Not authorized")
return []
elif 'user not found' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter user not found, disconnecting twitter: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: User not found")
return []
elif '429' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter rate limited: %s: %s' %
(self.feed.log_title[:30], self.address, e))
self.feed.save_feed_history(560, "Twitter Error: Rate limited")
return []
elif 'blocked from viewing' in message:
logging.debug(u' ***> [%-30s] ~FRTwitter user blocked, ignoring: %s' %
(self.feed.log_title[:30], e))
self.feed.save_feed_history(560, "Twitter Error: Blocked from viewing")
return []
else:
raise e
if not tweets:
return []
return tweets
def tweet_story(self, user_tweet):
categories = set()

View file

@ -1,290 +0,0 @@
"""$Id: __init__.py 699 2006-09-25 02:01:18Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 699 $"
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
import socket
if hasattr(socket, 'setdefaulttimeout'):
socket.setdefaulttimeout(10)
Timeout = socket.timeout
else:
import timeoutsocket
timeoutsocket.setDefaultSocketTimeout(10)
Timeout = timeoutsocket.Timeout
import urllib2
import logging
from logging import *
from xml.sax import SAXException
from xml.sax.xmlreader import InputSource
import re
import xmlEncoding
import mediaTypes
from httplib import BadStatusLine
MAXDATALENGTH = 200000
def _validate(aString, firstOccurrenceOnly, loggedEvents, base, encoding, selfURIs=None):
"""validate RSS from string, returns validator object"""
from xml.sax import make_parser, handler
from base import SAXDispatcher
from exceptions import UnicodeError
from cStringIO import StringIO
# By now, aString should be Unicode
source = InputSource()
source.setByteStream(StringIO(xmlEncoding.asUTF8(aString)))
validator = SAXDispatcher(base, selfURIs or [base], encoding)
validator.setFirstOccurrenceOnly(firstOccurrenceOnly)
validator.loggedEvents += loggedEvents
# experimental RSS-Profile draft 1.06 support
validator.setLiterals(re.findall('&#x26;(\w+);',aString))
xmlver = re.match("^<\?\s*xml\s+version\s*=\s*['\"]([-a-zA-Z0-9_.:]*)['\"]",aString)
if xmlver and xmlver.group(1)<>'1.0':
validator.log(logging.BadXmlVersion({"version":xmlver.group(1)}))
try:
from xml.sax.expatreader import ExpatParser
class fake_dtd_parser(ExpatParser):
def reset(self):
ExpatParser.reset(self)
self._parser.UseForeignDTD(1)
parser = fake_dtd_parser()
except:
parser = make_parser()
parser.setFeature(handler.feature_namespaces, 1)
parser.setContentHandler(validator)
parser.setErrorHandler(validator)
parser.setEntityResolver(validator)
if hasattr(parser, '_ns_stack'):
# work around bug in built-in SAX parser (doesn't recognize xml: namespace)
# PyXML doesn't have this problem, and it doesn't have _ns_stack either
parser._ns_stack.append({'http://www.w3.org/XML/1998/namespace':'xml'})
def xmlvalidate(log):
import libxml2
from StringIO import StringIO
from random import random
prefix="...%s..." % str(random()).replace('0.','')
msg=[]
libxml2.registerErrorHandler(lambda msg,str: msg.append(str), msg)
input = libxml2.inputBuffer(StringIO(xmlEncoding.asUTF8(aString)))
reader = input.newTextReader(prefix)
reader.SetParserProp(libxml2.PARSER_VALIDATE, 1)
ret = reader.Read()
while ret == 1: ret = reader.Read()
msg=''.join(msg)
for line in msg.splitlines():
if line.startswith(prefix): log(line.split(':',4)[-1].strip())
validator.xmlvalidator=xmlvalidate
try:
parser.parse(source)
except SAXException:
pass
except UnicodeError:
import sys
exctype, value = sys.exc_info()[:2]
validator.log(logging.UnicodeError({"exception":value}))
if validator.getFeedType() == TYPE_RSS1:
try:
from rdflib.syntax.parsers.RDFXMLHandler import RDFXMLHandler
class Handler(RDFXMLHandler):
ns_prefix_map = {}
prefix_ns_map = {}
def add(self, triple): pass
def __init__(self, dispatcher):
RDFXMLHandler.__init__(self, self)
self.dispatcher=dispatcher
def error(self, message):
self.dispatcher.log(InvalidRDF({"message": message}))
source.getByteStream().reset()
parser.reset()
parser.setContentHandler(Handler(parser.getContentHandler()))
parser.setErrorHandler(handler.ErrorHandler())
parser.parse(source)
except:
pass
return validator
def validateStream(aFile, firstOccurrenceOnly=0, contentType=None, base=""):
loggedEvents = []
if contentType:
(mediaType, charset) = mediaTypes.checkValid(contentType, loggedEvents)
else:
(mediaType, charset) = (None, None)
rawdata = aFile.read(MAXDATALENGTH)
if aFile.read(1):
raise ValidationFailure(logging.ValidatorLimit({'limit': 'feed length > ' + str(MAXDATALENGTH) + ' bytes'}))
encoding, rawdata = xmlEncoding.decode(mediaType, charset, rawdata, loggedEvents, fallback='utf-8')
validator = _validate(rawdata, firstOccurrenceOnly, loggedEvents, base, encoding)
if mediaType and validator.feedType:
mediaTypes.checkAgainstFeedType(mediaType, validator.feedType, validator.loggedEvents)
return {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
def validateString(aString, firstOccurrenceOnly=0, fallback=None, base=""):
loggedEvents = []
if type(aString) != unicode:
encoding, aString = xmlEncoding.decode("", None, aString, loggedEvents, fallback)
else:
encoding = "utf-8" # setting a sane (?) default
if aString is not None:
validator = _validate(aString, firstOccurrenceOnly, loggedEvents, base, encoding)
return {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
else:
return {"loggedEvents": loggedEvents}
def validateURL(url, firstOccurrenceOnly=1, wantRawData=0):
"""validate RSS from URL, returns events list, or (events, rawdata) tuple"""
loggedEvents = []
request = urllib2.Request(url)
request.add_header("Accept-encoding", "gzip, deflate")
request.add_header("User-Agent", "FeedValidator/1.3")
usock = None
try:
try:
usock = urllib2.urlopen(request)
rawdata = usock.read(MAXDATALENGTH)
if usock.read(1):
raise ValidationFailure(logging.ValidatorLimit({'limit': 'feed length > ' + str(MAXDATALENGTH) + ' bytes'}))
# check for temporary redirects
if usock.geturl()<>request.get_full_url():
from httplib import HTTPConnection
spliturl=url.split('/',3)
if spliturl[0]=="http:":
conn=HTTPConnection(spliturl[2])
conn.request("GET",'/'+spliturl[3].split("#",1)[0])
resp=conn.getresponse()
if resp.status<>301:
loggedEvents.append(TempRedirect({}))
except BadStatusLine, status:
raise ValidationFailure(logging.HttpError({'status': status.__class__}))
except urllib2.HTTPError, status:
rawdata = status.read()
lastline = rawdata.strip().split('\n')[-1].strip()
if lastline in ['</rss>','</feed>','</rdf:RDF>']:
loggedEvents.append(logging.HttpError({'status': status}))
usock = status
else:
raise ValidationFailure(logging.HttpError({'status': status}))
except urllib2.URLError, x:
raise ValidationFailure(logging.HttpError({'status': x.reason}))
except Timeout, x:
raise ValidationFailure(logging.IOError({"message": 'Server timed out', "exception":x}))
if usock.headers.get('content-encoding', None) == None:
loggedEvents.append(Uncompressed({}))
if usock.headers.get('content-encoding', None) == 'gzip':
import gzip, StringIO
try:
rawdata = gzip.GzipFile(fileobj=StringIO.StringIO(rawdata)).read()
except:
import sys
exctype, value = sys.exc_info()[:2]
event=logging.IOError({"message": 'Server response declares Content-Encoding: gzip', "exception":value})
raise ValidationFailure(event)
if usock.headers.get('content-encoding', None) == 'deflate':
import zlib
try:
rawdata = zlib.decompress(rawdata, -zlib.MAX_WBITS)
except:
import sys
exctype, value = sys.exc_info()[:2]
event=logging.IOError({"message": 'Server response declares Content-Encoding: deflate', "exception":value})
raise ValidationFailure(event)
mediaType = None
charset = None
# Is the Content-Type correct?
contentType = usock.headers.get('content-type', None)
if contentType:
(mediaType, charset) = mediaTypes.checkValid(contentType, loggedEvents)
# Check for malformed HTTP headers
for (h, v) in usock.headers.items():
if (h.find(' ') >= 0):
loggedEvents.append(HttpProtocolError({'header': h}))
selfURIs = [request.get_full_url()]
baseURI = usock.geturl()
if not baseURI in selfURIs: selfURIs.append(baseURI)
# Get baseURI from content-location and/or redirect information
if usock.headers.get('content-location', None):
from urlparse import urljoin
baseURI=urljoin(baseURI,usock.headers.get('content-location', ""))
elif usock.headers.get('location', None):
from urlparse import urljoin
baseURI=urljoin(baseURI,usock.headers.get('location', ""))
if not baseURI in selfURIs: selfURIs.append(baseURI)
usock.close()
usock = None
mediaTypes.contentSniffing(mediaType, rawdata, loggedEvents)
encoding, rawdata = xmlEncoding.decode(mediaType, charset, rawdata, loggedEvents, fallback='utf-8')
if rawdata is None:
return {'loggedEvents': loggedEvents}
rawdata = rawdata.replace('\r\n', '\n').replace('\r', '\n') # normalize EOL
validator = _validate(rawdata, firstOccurrenceOnly, loggedEvents, baseURI, encoding, selfURIs)
# Warn about mismatches between media type and feed version
if mediaType and validator.feedType:
mediaTypes.checkAgainstFeedType(mediaType, validator.feedType, validator.loggedEvents)
params = {"feedType":validator.feedType, "loggedEvents":validator.loggedEvents}
if wantRawData:
params['rawdata'] = rawdata
return params
finally:
try:
if usock: usock.close()
except:
pass
__all__ = ['base',
'channel',
'compatibility',
'image',
'item',
'logging',
'rdf',
'root',
'rss',
'skipHours',
'textInput',
'util',
'validators',
'validateURL',
'validateString']

View file

@ -1,53 +0,0 @@
"""$Id: author.py 699 2006-09-25 02:01:18Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 699 $"
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from base import validatorBase
from validators import *
#
# author element.
#
class author(validatorBase):
def getExpectedAttrNames(self):
return [(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'parseType')]
def validate(self):
if not "name" in self.children and not "atom_name" in self.children:
self.log(MissingElement({"parent":self.name, "element":"name"}))
def do_name(self):
return nonhtml(), nonemail(), nonblank(), noduplicates()
def do_email(self):
return addr_spec(), noduplicates()
def do_uri(self):
return nonblank(), rfc3987(), nows(), noduplicates()
def do_foaf_workplaceHomepage(self):
return rdfResourceURI()
def do_foaf_homepage(self):
return rdfResourceURI()
def do_foaf_weblog(self):
return rdfResourceURI()
def do_foaf_plan(self):
return text()
def do_foaf_firstName(self):
return text()
def do_xhtml_div(self):
from content import diveater
return diveater()
# RSS/Atom support
do_atom_name = do_name
do_atom_email = do_email
do_atom_uri = do_uri

View file

@ -1,511 +0,0 @@
"""$Id: base.py 744 2007-03-24 11:57:16Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 744 $"
__date__ = "$Date: 2007-03-24 11:57:16 +0000 (Sat, 24 Mar 2007) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from xml.sax.handler import ContentHandler
from xml.sax.xmlreader import Locator
from logging import NonCanonicalURI, NotUTF8
import re
# references:
# http://web.resource.org/rss/1.0/modules/standard.html
# http://web.resource.org/rss/1.0/modules/proposed.html
# http://dmoz.org/Reference/Libraries/Library_and_Information_Science/Technical_Services/Cataloguing/Metadata/RDF/Applications/RSS/Specifications/RSS1.0_Modules/
namespaces = {
"http://www.bloglines.com/about/specs/fac-1.0": "access",
"http://webns.net/mvcb/": "admin",
"http://purl.org/rss/1.0/modules/aggregation/": "ag",
"http://purl.org/rss/1.0/modules/annotate/": "annotate",
"http://media.tangent.org/rss/1.0/": "audio",
"http://backend.userland.com/blogChannelModule": "blogChannel",
"http://web.resource.org/cc/": "cc",
"http://www.microsoft.com/schemas/rss/core/2005": "cf",
"http://backend.userland.com/creativeCommonsRssModule": "creativeCommons",
"http://purl.org/rss/1.0/modules/company": "company",
"http://purl.org/rss/1.0/modules/content/": "content",
"http://my.theinfo.org/changed/1.0/rss/": "cp",
"http://purl.org/dc/elements/1.1/": "dc",
"http://purl.org/dc/terms/": "dcterms",
"http://purl.org/rss/1.0/modules/email/": "email",
"http://purl.org/rss/1.0/modules/event/": "ev",
"http://www.w3.org/2003/01/geo/wgs84_pos#": "geo",
"http://geourl.org/rss/module/": "geourl",
"http://www.georss.org/georss": "georss",
"http://www.opengis.net/gml": "gml",
"http://postneo.com/icbm": "icbm",
"http://purl.org/rss/1.0/modules/image/": "image",
"http://www.itunes.com/dtds/podcast-1.0.dtd": "itunes",
"http://xmlns.com/foaf/0.1/": "foaf",
"http://purl.org/rss/1.0/modules/link/": "l",
"http://search.yahoo.com/mrss/": "media",
"http://a9.com/-/spec/opensearch/1.1/": "opensearch",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#": "rdf",
"http://www.w3.org/2000/01/rdf-schema#": "rdfs",
"http://purl.org/rss/1.0/modules/reference/": "ref",
"http://purl.org/rss/1.0/modules/richequiv/": "reqv",
"http://purl.org/rss/1.0/modules/rss091#": "rss091",
"http://purl.org/rss/1.0/modules/search/": "search",
"http://purl.org/rss/1.0/modules/slash/": "slash",
"http://purl.org/rss/1.0/modules/servicestatus/": "ss",
"http://hacks.benhammersley.com/rss/streaming/": "str",
"http://purl.org/rss/1.0/modules/subscription/": "sub",
"http://purl.org/rss/1.0/modules/syndication/": "sy",
"http://purl.org/rss/1.0/modules/taxonomy/": "taxo",
"http://purl.org/rss/1.0/modules/threading/": "thr",
"http://purl.org/syndication/thread/1.0": "thr",
"http://madskills.com/public/xml/rss/module/trackback/": "trackback",
"http://wellformedweb.org/CommentAPI/": "wfw",
"http://purl.org/rss/1.0/modules/wiki/": "wiki",
"http://www.usemod.com/cgi-bin/mb.pl?ModWiki": "wiki",
"http://schemas.xmlsoap.org/soap/envelope/": "soap",
"http://www.w3.org/2005/Atom": "atom",
"http://www.w3.org/1999/xhtml": "xhtml",
"http://my.netscape.com/rdf/simple/0.9/": "rss090",
"http://purl.org/net/rss1.1#": "rss11",
"http://base.google.com/ns/1.0": "g",
"http://www.w3.org/XML/1998/namespace": "xml",
"http://openid.net/xmlns/1.0": "openid",
"xri://$xrd*($v*2.0)": "xrd",
"xri://$xrds": "xrds",
}
def near_miss(ns):
try:
return re.match(".*\w", ns).group().lower()
except:
return ns
nearly_namespaces = dict([(near_miss(u),p) for u,p in namespaces.items()])
stdattrs = [(u'http://www.w3.org/XML/1998/namespace', u'base'),
(u'http://www.w3.org/XML/1998/namespace', u'lang'),
(u'http://www.w3.org/XML/1998/namespace', u'space')]
#
# From the SAX parser's point of view, this class is the one responsible for
# handling SAX events. In actuality, all this class does is maintain a
# pushdown stack of the *real* content handlers, and delegates sax events
# to the current one.
#
class SAXDispatcher(ContentHandler):
firstOccurrenceOnly = 0
def __init__(self, base, selfURIs, encoding):
from root import root
ContentHandler.__init__(self)
self.lastKnownLine = 1
self.lastKnownColumn = 0
self.loggedEvents = []
self.feedType = 0
try:
self.xmlBase = base.encode('idna')
except:
self.xmlBase = base
self.selfURIs = selfURIs
self.encoding = encoding
self.handler_stack=[[root(self, base)]]
self.literal_entities=[]
self.defaultNamespaces = []
# experimental RSS-Profile draft 1.06 support
def setLiterals(self, literals):
for literal in literals:
if literal not in self.literal_entities:
self.literal_entities.append(literal)
def setDocumentLocator(self, locator):
self.locator = locator
ContentHandler.setDocumentLocator(self, self.locator)
def setFirstOccurrenceOnly(self, firstOccurrenceOnly=1):
self.firstOccurrenceOnly = firstOccurrenceOnly
def startPrefixMapping(self, prefix, uri):
for handler in iter(self.handler_stack[-1]):
handler.namespace[prefix] = uri
if uri and len(uri.split())>1:
from xml.sax import SAXException
self.error(SAXException('Invalid Namespace: %s' % uri))
if prefix in namespaces.values():
if not namespaces.get(uri,'') == prefix and prefix:
from logging import ReservedPrefix
preferredURI = [key for key, value in namespaces.items() if value == prefix][0]
self.log(ReservedPrefix({'prefix':prefix, 'ns':preferredURI}))
elif prefix=='wiki' and uri.find('usemod')>=0:
from logging import ObsoleteWikiNamespace
self.log(ObsoleteWikiNamespace({'preferred':namespaces[uri], 'ns':uri}))
elif namespaces.has_key(uri):
if not namespaces[uri] == prefix and prefix:
from logging import NonstdPrefix
self.log(NonstdPrefix({'preferred':namespaces[uri], 'ns':uri}))
def namespaceFor(self, prefix):
return None
def startElementNS(self, name, qname, attrs):
self.lastKnownLine = self.locator.getLineNumber()
self.lastKnownColumn = self.locator.getColumnNumber()
qname, name = name
for handler in iter(self.handler_stack[-1]):
handler.startElementNS(name, qname, attrs)
if len(attrs):
present = attrs.getNames()
unexpected = filter(lambda x: x not in stdattrs, present)
for handler in iter(self.handler_stack[-1]):
ean = handler.getExpectedAttrNames()
if ean: unexpected = filter(lambda x: x not in ean, unexpected)
for u in unexpected:
if u[0] and near_miss(u[0]) not in nearly_namespaces:
feedtype=self.getFeedType()
if (not qname) and feedtype and (feedtype==TYPE_RSS2):
from logging import InvalidExtensionAttr
self.log(InvalidExtensionAttr({"attribute":u, "element":name}))
continue
from logging import UnexpectedAttribute
if not u[0]: u=u[1]
self.log(UnexpectedAttribute({"parent":name, "attribute":u, "element":name}))
def resolveEntity(self, publicId, systemId):
if not publicId and not systemId:
import cStringIO
return cStringIO.StringIO()
try:
def log(exception):
from logging import SAXError
self.log(SAXError({'exception':str(exception)}))
if self.xmlvalidator:
self.xmlvalidator(log)
self.xmlvalidator=0
except:
pass
if (publicId=='-//Netscape Communications//DTD RSS 0.91//EN' and
systemId=='http://my.netscape.com/publish/formats/rss-0.91.dtd'):
from logging import ValidDoctype, DeprecatedDTD
self.log(ValidDoctype({}))
self.log(DeprecatedDTD({}))
else:
from logging import ContainsSystemEntity
self.lastKnownLine = self.locator.getLineNumber()
self.lastKnownColumn = self.locator.getColumnNumber()
self.log(ContainsSystemEntity({}))
from StringIO import StringIO
return StringIO()
def skippedEntity(self, name):
from logging import ValidDoctype
if [e for e in self.loggedEvents if e.__class__ == ValidDoctype]:
from htmlentitydefs import name2codepoint
if name in name2codepoint: return
from logging import UndefinedNamedEntity
self.log(UndefinedNamedEntity({'value':name}))
def characters(self, string):
self.lastKnownLine = self.locator.getLineNumber()
self.lastKnownColumn = self.locator.getColumnNumber()
for handler in iter(self.handler_stack[-1]):
handler.characters(string)
def endElementNS(self, name, qname):
self.lastKnownLine = self.locator.getLineNumber()
self.lastKnownColumn = self.locator.getColumnNumber()
qname, name = name
for handler in iter(self.handler_stack[-1]):
handler.endElementNS(name, qname)
del self.handler_stack[-1]
def push(self, handlers, name, attrs, parent):
if hasattr(handlers,'__iter__'):
for handler in iter(handlers):
handler.setElement(name, attrs, parent)
handler.value=""
handler.prevalidate()
else:
handlers.setElement(name, attrs, parent)
handlers.value=""
handlers.prevalidate()
handlers = [handlers]
self.handler_stack.append(handlers)
def log(self, event, offset=(0,0)):
def findDuplicate(self, event):
duplicates = [e for e in self.loggedEvents if e.__class__ == event.__class__]
if duplicates and (event.__class__ in [NonCanonicalURI]):
return duplicates[0]
for dup in duplicates:
for k, v in event.params.items():
if k != 'value':
if not k in dup.params or dup.params[k] != v: break
else:
return dup
if event.params.has_key('element') and event.params['element']:
if not isinstance(event.params['element'],tuple):
event.params['element']=':'.join(event.params['element'].split('_', 1))
elif event.params['element'][0]==u'http://www.w3.org/XML/1998/namespace':
event.params['element'] = 'xml:' + event.params['element'][-1]
if self.firstOccurrenceOnly:
dup = findDuplicate(self, event)
if dup:
dup.params['msgcount'] = dup.params['msgcount'] + 1
return
event.params['msgcount'] = 1
try:
line = self.locator.getLineNumber() + offset[0]
backupline = self.lastKnownLine
column = (self.locator.getColumnNumber() or 0) + offset[1]
backupcolumn = self.lastKnownColumn
except AttributeError:
line = backupline = column = backupcolumn = 1
event.params['line'] = line
event.params['backupline'] = backupline
event.params['column'] = column
event.params['backupcolumn'] = backupcolumn
self.loggedEvents.append(event)
def error(self, exception):
from logging import SAXError
self.log(SAXError({'exception':str(exception)}))
raise exception
fatalError=error
warning=error
def getFeedType(self):
return self.feedType
def setFeedType(self, feedType):
self.feedType = feedType
#
# This base class for content handlers keeps track of such administrative
# details as the parent of the current element, and delegating both log
# and push events back up the stack. It will also concatenate up all of
# the SAX events associated with character data into a value, handing such
# things as CDATA and entities.
#
# Subclasses are expected to declare "do_name" methods for every
# element that they support. These methods are expected to return the
# appropriate handler for the element.
#
# The name of the element and the names of the children processed so
# far are also maintained.
#
# Hooks are also provided for subclasses to do "prevalidation" and
# "validation".
#
from logging import TYPE_RSS2
class validatorBase(ContentHandler):
def __init__(self):
ContentHandler.__init__(self)
self.value = ""
self.attrs = None
self.children = []
self.isValid = 1
self.name = None
self.itunes = False
self.namespace = {}
def setElement(self, name, attrs, parent):
self.name = name
self.attrs = attrs
self.parent = parent
self.dispatcher = parent.dispatcher
self.line = self.dispatcher.locator.getLineNumber()
self.col = self.dispatcher.locator.getColumnNumber()
self.xmlLang = parent.xmlLang
if attrs and attrs.has_key((u'http://www.w3.org/XML/1998/namespace', u'base')):
self.xmlBase=attrs.getValue((u'http://www.w3.org/XML/1998/namespace', u'base'))
from validators import rfc3987
self.validate_attribute((u'http://www.w3.org/XML/1998/namespace',u'base'),
rfc3987)
from urlparse import urljoin
self.xmlBase = urljoin(parent.xmlBase, self.xmlBase)
else:
self.xmlBase = parent.xmlBase
return self
def simplename(self, name):
if not name[0]: return name[1]
return namespaces.get(name[0], name[0]) + ":" + name[1]
def namespaceFor(self, prefix):
if self.namespace.has_key(prefix):
return self.namespace[prefix]
elif self.parent:
return self.parent.namespaceFor(prefix)
else:
return None
def validate_attribute(self, name, rule):
if not isinstance(rule,validatorBase): rule = rule()
if isinstance(name,str): name = (None,name)
rule.setElement(self.simplename(name), {}, self)
rule.value=self.attrs.getValue(name)
rule.validate()
def validate_required_attribute(self, name, rule):
if self.attrs and self.attrs.has_key(name):
self.validate_attribute(name, rule)
else:
from logging import MissingAttribute
self.log(MissingAttribute({"attr": self.simplename(name)}))
def validate_optional_attribute(self, name, rule):
if self.attrs and self.attrs.has_key(name):
self.validate_attribute(name, rule)
def getExpectedAttrNames(self):
None
def unknown_starttag(self, name, qname, attrs):
from validators import any
return any(self, name, qname, attrs)
def startElementNS(self, name, qname, attrs):
if attrs.has_key((u'http://www.w3.org/XML/1998/namespace', u'lang')):
self.xmlLang=attrs.getValue((u'http://www.w3.org/XML/1998/namespace', u'lang'))
if self.xmlLang:
from validators import iso639_validate
iso639_validate(self.log, self.xmlLang, "xml:lang", name)
from validators import eater
feedtype=self.getFeedType()
if (not qname) and feedtype and (feedtype!=TYPE_RSS2):
from logging import UndeterminableVocabulary
self.log(UndeterminableVocabulary({"parent":self.name, "element":name, "namespace":'""'}))
qname="null"
if qname in self.dispatcher.defaultNamespaces: qname=None
nm_qname = near_miss(qname)
if nearly_namespaces.has_key(nm_qname):
prefix = nearly_namespaces[nm_qname]
qname, name = None, prefix + "_" + name
if prefix == 'itunes' and not self.itunes and not self.parent.itunes:
if hasattr(self, 'setItunes'): self.setItunes(True)
# ensure all attribute namespaces are properly defined
for (namespace,attr) in attrs.keys():
if ':' in attr and not namespace:
from logging import MissingNamespace
self.log(MissingNamespace({"parent":self.name, "element":attr}))
if qname=='http://purl.org/atom/ns#':
from logging import ObsoleteNamespace
self.log(ObsoleteNamespace({"element":"feed"}))
for key, string in attrs.items():
for c in string:
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
from validators import BadCharacters
self.log(BadCharacters({"parent":name, "element":key[-1]}))
if qname:
handler = self.unknown_starttag(name, qname, attrs)
name="unknown_"+name
else:
try:
self.child=name
if name.startswith('dc_'):
# handle "Qualified" Dublin Core
handler = getattr(self, "do_" + name.replace("-","_").split('.')[0])()
else:
handler = getattr(self, "do_" + name.replace("-","_"))()
except AttributeError:
if name.find(':') != -1:
from logging import MissingNamespace
self.log(MissingNamespace({"parent":self.name, "element":name}))
handler = eater()
elif name.startswith('xhtml_'):
from logging import MisplacedXHTMLContent
self.log(MisplacedXHTMLContent({"parent": ':'.join(self.name.split("_",1)), "element":name}))
handler = eater()
else:
from logging import UndefinedElement
self.log(UndefinedElement({"parent": ':'.join(self.name.split("_",1)), "element":name}))
handler = eater()
self.push(handler, name, attrs)
# MAP - always append name, even if already exists (we need this to
# check for too many hour elements in skipHours, and it doesn't
# hurt anything else)
self.children.append(name)
def normalizeWhitespace(self):
self.value = self.value.strip()
def endElementNS(self, name, qname):
self.normalizeWhitespace()
self.validate()
if self.isValid and self.name:
from validators import ValidElement
self.log(ValidElement({"parent":self.parent.name, "element":name}))
def textOK(self):
from validators import UnexpectedText
self.log(UnexpectedText({"element":self.name,"parent":self.parent.name}))
def characters(self, string):
if string.strip(): self.textOK()
line=column=0
pc=' '
for c in string:
# latin characters double encoded as utf-8
if 0x80 <= ord(c) <= 0xBF:
if 0xC2 <= ord(pc) <= 0xC3:
try:
string.encode('iso-8859-1').decode('utf-8')
from validators import BadCharacters
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}), offset=(line,max(1,column-1)))
except:
pass
pc = c
# win1252
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
from validators import BadCharacters
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}), offset=(line,column))
column=column+1
if ord(c) in (10,13):
column=0
line=line+1
self.value = self.value + string
def log(self, event, offset=(0,0)):
if not event.params.has_key('element'):
event.params['element'] = self.name
self.dispatcher.log(event, offset)
self.isValid = 0
def setFeedType(self, feedType):
self.dispatcher.setFeedType(feedType)
def getFeedType(self):
return self.dispatcher.getFeedType()
def push(self, handler, name, value):
self.dispatcher.push(handler, name, value, self)
def leaf(self):
from validators import text
return text()
def prevalidate(self):
pass
def validate(self):
pass

View file

@ -1,30 +0,0 @@
"""$Id: category.py 699 2006-09-25 02:01:18Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 699 $"
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from base import validatorBase
from validators import *
#
# author element.
#
class category(validatorBase, rfc3987_full, nonhtml):
def getExpectedAttrNames(self):
return [(None,u'term'),(None,u'scheme'),(None,u'label')]
def prevalidate(self):
self.children.append(True) # force warnings about "mixed" content
if not self.attrs.has_key((None,"term")):
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"term"}))
if self.attrs.has_key((None,"scheme")):
self.value=self.attrs.getValue((None,"scheme"))
rfc3987_full.validate(self, extraParams={"element": "scheme"})
if self.attrs.has_key((None,"label")):
self.value=self.attrs.getValue((None,"label"))
nonhtml.validate(self)

View file

@ -1,20 +0,0 @@
# http://msdn.microsoft.com/XML/rss/sle/default.aspx
from base import validatorBase
from validators import eater, text
class sort(validatorBase):
def getExpectedAttrNames(self):
return [(None,u'data-type'),(None,u'default'),(None,u'element'),(None, u'label'),(None,u'ns')]
class group(validatorBase):
def getExpectedAttrNames(self):
return [(None,u'element'),(None, u'label'),(None,u'ns')]
class listinfo(validatorBase):
def do_cf_sort(self):
return sort()
def do_cf_group(self):
return group()
class treatAs(text): pass

View file

@ -1,279 +0,0 @@
"""$Id: channel.py 711 2006-10-25 00:43:41Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 711 $"
__date__ = "$Date: 2006-10-25 00:43:41 +0000 (Wed, 25 Oct 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from base import validatorBase
from logging import *
from validators import *
from itunes import itunes_channel
from extension import *
#
# channel element.
#
class channel(validatorBase, rfc2396, extension_channel, itunes_channel):
def __init__(self):
self.link=None
validatorBase.__init__(self)
def validate(self):
if not "description" in self.children:
self.log(MissingDescription({"parent":self.name,"element":"description"}))
if not "link" in self.children:
self.log(MissingLink({"parent":self.name, "element":"link"}))
if not "title" in self.children:
self.log(MissingTitle({"parent":self.name, "element":"title"}))
if not "dc_language" in self.children and not "language" in self.children:
if not self.xmlLang:
self.log(MissingDCLanguage({"parent":self.name, "element":"language"}))
if self.children.count("image") > 1:
self.log(DuplicateElement({"parent":self.name, "element":"image"}))
if self.children.count("textInput") > 1:
self.log(DuplicateElement({"parent":self.name, "element":"textInput"}))
if self.children.count("skipHours") > 1:
self.log(DuplicateElement({"parent":self.name, "element":"skipHours"}))
if self.children.count("skipDays") > 1:
self.log(DuplicateElement({"parent":self.name, "element":"skipDays"}))
if self.attrs.has_key((rdfNS,"about")):
self.value = self.attrs.getValue((rdfNS, "about"))
rfc2396.validate(self, extraParams={"attr": "rdf:about"})
if not "items" in self.children:
self.log(MissingElement({"parent":self.name, "element":"items"}))
if self.itunes: itunes_channel.validate(self)
def do_image(self):
from image import image
return image(), noduplicates()
def do_textInput(self):
from textInput import textInput
return textInput(), noduplicates()
def do_textinput(self):
if not self.attrs.has_key((rdfNS,"about")):
# optimize for RSS 2.0. If it is not valid RDF, assume that it is
# a simple misspelling (in other words, the error message will be
# less than helpful on RSS 1.0 feeds.
self.log(UndefinedElement({"parent":self.name, "element":"textinput"}))
return eater(), noduplicates()
def do_link(self):
return link(), noduplicates()
def do_title(self):
return nonhtml(), noduplicates(), nonblank()
def do_description(self):
return nonhtml(), noduplicates()
def do_blink(self):
return blink(), noduplicates()
def do_atom_author(self):
from author import author
return author()
def do_atom_category(self):
from category import category
return category()
def do_atom_contributor(self):
from author import author
return author()
def do_atom_generator(self):
from generator import generator
return generator(), nonblank(), noduplicates()
def do_atom_id(self):
return rfc2396_full(), noduplicates()
def do_atom_icon(self):
return nonblank(), rfc2396(), noduplicates()
def do_atom_link(self):
from link import link
return link()
def do_atom_logo(self):
return nonblank(), rfc2396(), noduplicates()
def do_atom_title(self):
from content import textConstruct
return textConstruct(), noduplicates()
def do_atom_subtitle(self):
from content import textConstruct
return textConstruct(), noduplicates()
def do_atom_rights(self):
from content import textConstruct
return textConstruct(), noduplicates()
def do_atom_updated(self):
return rfc3339(), noduplicates()
def do_dc_creator(self):
if "managingEditor" in self.children:
self.log(DuplicateSemantics({"core":"managingEditor", "ext":"dc:creator"}))
return text() # duplicates allowed
def do_dc_subject(self):
if "category" in self.children:
self.log(DuplicateSemantics({"core":"category", "ext":"dc:subject"}))
return text() # duplicates allowed
def do_dc_date(self):
if "pubDate" in self.children:
self.log(DuplicateSemantics({"core":"pubDate", "ext":"dc:date"}))
return w3cdtf(), noduplicates()
def do_cc_license(self):
if "creativeCommons_license" in self.children:
self.log(DuplicateSemantics({"core":"creativeCommons:license", "ext":"cc:license"}))
return eater()
def do_creativeCommons_license(self):
if "cc_license" in self.children:
self.log(DuplicateSemantics({"core":"creativeCommons:license", "ext":"cc:license"}))
return rfc2396_full()
class rss20Channel(channel):
def do_item(self):
from item import rss20Item
return rss20Item()
def do_category(self):
return category()
def do_cloud(self):
return cloud(), noduplicates()
do_rating = validatorBase.leaf # TODO test cases?!?
def do_ttl(self):
return positiveInteger(), nonblank(), noduplicates()
def do_docs(self):
return rfc2396_full(), noduplicates()
def do_generator(self):
if "admin_generatorAgent" in self.children:
self.log(DuplicateSemantics({"core":"generator", "ext":"admin:generatorAgent"}))
return text(), noduplicates()
def do_pubDate(self):
if "dc_date" in self.children:
self.log(DuplicateSemantics({"core":"pubDate", "ext":"dc:date"}))
return rfc822(), noduplicates()
def do_managingEditor(self):
if "dc_creator" in self.children:
self.log(DuplicateSemantics({"core":"managingEditor", "ext":"dc:creator"}))
return email(), noduplicates()
def do_webMaster(self):
if "dc_publisher" in self.children:
self.log(DuplicateSemantics({"core":"webMaster", "ext":"dc:publisher"}))
return email(), noduplicates()
def do_language(self):
if "dc_language" in self.children:
self.log(DuplicateSemantics({"core":"language", "ext":"dc:language"}))
return iso639(), noduplicates()
def do_copyright(self):
if "dc_rights" in self.children:
self.log(DuplicateSemantics({"core":"copyright", "ext":"dc:rights"}))
return nonhtml(), noduplicates()
def do_lastBuildDate(self):
if "dcterms_modified" in self.children:
self.log(DuplicateSemantics({"core":"lastBuildDate", "ext":"dcterms:modified"}))
return rfc822(), noduplicates()
def do_skipHours(self):
from skipHours import skipHours
return skipHours()
def do_skipDays(self):
from skipDays import skipDays
return skipDays()
class rss10Channel(channel):
def getExpectedAttrNames(self):
return [(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'about'),
(u'http://www.w3.org/1999/02/22-rdf-syntax-ns#', u'about')]
def prevalidate(self):
if self.attrs.has_key((rdfNS,"about")):
if not "abouts" in self.dispatcher.__dict__:
self.dispatcher.__dict__["abouts"] = []
self.dispatcher.__dict__["abouts"].append(self.attrs[(rdfNS,"about")])
def do_items(self): # this actually should be from the rss1.0 ns
if not self.attrs.has_key((rdfNS,"about")):
self.log(MissingAttribute({"parent":self.name, "element":self.name, "attr":"rdf:about"}))
from item import items
return items(), noduplicates()
def do_rdfs_label(self):
return text()
def do_rdfs_comment(self):
return text()
class link(rfc2396_full):
def validate(self):
self.parent.link = self.value
rfc2396_full.validate(self)
class blink(text):
def validate(self):
self.log(NoBlink({}))
class category(nonhtml):
def getExpectedAttrNames(self):
return [(None, u'domain')]
class cloud(validatorBase):
def getExpectedAttrNames(self):
return [(None, u'domain'), (None, u'path'), (None, u'registerProcedure'),
(None, u'protocol'), (None, u'port')]
def prevalidate(self):
if (None, 'domain') not in self.attrs.getNames():
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"domain"}))
else:
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"domain"}))
try:
if int(self.attrs.getValue((None, 'port'))) <= 0:
self.log(InvalidIntegerAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
else:
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":'port'}))
except KeyError:
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
except ValueError:
self.log(InvalidIntegerAttribute({"parent":self.parent.name, "element":self.name, "attr":'port'}))
if (None, 'path') not in self.attrs.getNames():
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"path"}))
else:
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"path"}))
if (None, 'registerProcedure') not in self.attrs.getNames():
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"registerProcedure"}))
else:
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"registerProcedure"}))
if (None, 'protocol') not in self.attrs.getNames():
self.log(MissingAttribute({"parent":self.parent.name, "element":self.name, "attr":"protocol"}))
else:
self.log(ValidCloud({"parent":self.parent.name, "element":self.name, "attr":"protocol"}))
## TODO - is there a list of accepted protocols for this thing?
return validatorBase.prevalidate(self)

View file

@ -1,37 +0,0 @@
"""$Id: compatibility.py 699 2006-09-25 02:01:18Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 699 $"
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from logging import *
def _must(event):
return isinstance(event, Error)
def _should(event):
return isinstance(event, Warning)
def _may(event):
return isinstance(event, Info)
def A(events):
return [event for event in events if _must(event)]
def AA(events):
return [event for event in events if _must(event) or _should(event)]
def AAA(events):
return [event for event in events if _must(event) or _should(event) or _may(event)]
def AAAA(events):
return events
def analyze(events, rawdata):
for event in events:
if isinstance(event,UndefinedElement):
if event.params['parent'] == 'root':
if event.params['element'].lower() in ['html','xhtml:html']:
return "html"
return None

View file

@ -1,151 +0,0 @@
"""$Id: content.py 699 2006-09-25 02:01:18Z rubys $"""
__author__ = "Sam Ruby <http://intertwingly.net/> and Mark Pilgrim <http://diveintomark.org/>"
__version__ = "$Revision: 699 $"
__date__ = "$Date: 2006-09-25 02:01:18 +0000 (Mon, 25 Sep 2006) $"
__copyright__ = "Copyright (c) 2002 Sam Ruby and Mark Pilgrim"
from base import validatorBase
from validators import *
from logging import *
#
# item element.
#
class textConstruct(validatorBase,rfc2396,nonhtml):
from validators import mime_re
import re
def getExpectedAttrNames(self):
return [(None, u'type'),(None, u'src')]
def normalizeWhitespace(self):
pass
def maptype(self):
if self.type.find('/') > -1:
self.log(InvalidTextType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
def prevalidate(self):
if self.attrs.has_key((None,"src")):
self.type=''
else:
self.type='text'
if self.getFeedType() == TYPE_RSS2 and self.name != 'atom_summary':
self.log(DuplicateDescriptionSemantics({"element":self.name}))
if self.attrs.has_key((None,"type")):
self.type=self.attrs.getValue((None,"type"))
if not self.type:
self.log(AttrNotBlank({"parent":self.parent.name, "element":self.name, "attr":"type"}))
self.maptype()
if self.attrs.has_key((None,"src")):
self.children.append(True) # force warnings about "mixed" content
self.value=self.attrs.getValue((None,"src"))
rfc2396.validate(self, errorClass=InvalidURIAttribute, extraParams={"attr": "src"})
self.value=""
if not self.attrs.has_key((None,"type")):
self.log(MissingTypeAttr({"parent":self.parent.name, "element":self.name, "attr":"type"}))
if self.type in ['text','html','xhtml'] and not self.attrs.has_key((None,"src")):
pass
elif self.type and not self.mime_re.match(self.type):
self.log(InvalidMIMEType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
else:
self.log(ValidMIMEAttribute({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))
if not self.xmlLang:
self.log(MissingDCLanguage({"parent":self.name, "element":"xml:lang"}))
def validate(self):
if self.type in ['text','xhtml']:
if self.type=='xhtml':
nonhtml.validate(self, NotInline)
else:
nonhtml.validate(self, ContainsUndeclaredHTML)
else:
if self.type.find('/') > -1 and not (
self.type.endswith('+xml') or self.type.endswith('/xml') or
self.type.startswith('text/')):
import base64
try:
self.value=base64.decodestring(self.value)
if self.type.endswith('/html'): self.type='html'
except:
self.log(NotBase64({"parent":self.parent.name, "element":self.name,"value":self.value}))
if self.type=='html' or self.type.endswith("/html"):
self.validateSafe(self.value)
if self.type.endswith("/html"):
if self.value.find("<html")<0 and not self.attrs.has_key((None,"src")):
self.log(HtmlFragment({"parent":self.parent.name, "element":self.name,"value":self.value, "type":self.type}))
else:
nonhtml.validate(self, ContainsUndeclaredHTML)
if not self.value and len(self.children)==0 and not self.attrs.has_key((None,"src")):
self.log(NotBlank({"parent":self.parent.name, "element":self.name}))
def textOK(self):
if self.children: validatorBase.textOK(self)
def characters(self, string):
for c in string:
if 0x80 <= ord(c) <= 0x9F or c == u'\ufffd':
from validators import BadCharacters
self.log(BadCharacters({"parent":self.parent.name, "element":self.name}))
if (self.type=='xhtml') and string.strip() and not self.value.strip():
self.log(MissingXhtmlDiv({"parent":self.parent.name, "element":self.name}))
validatorBase.characters(self,string)
def startElementNS(self, name, qname, attrs):
if (self.type<>'xhtml') and not (
self.type.endswith('+xml') or self.type.endswith('/xml')):
self.log(UndefinedElement({"parent":self.name, "element":name}))
if self.type=="xhtml":
if name<>'div' and not self.value.strip():
self.log(MissingXhtmlDiv({"parent":self.parent.name, "element":self.name}))
elif qname not in ["http://www.w3.org/1999/xhtml"]:
self.log(NotHtml({"parent":self.parent.name, "element":self.name, "message":"unexpected namespace: %s" % qname}))
if self.type=="application/xhtml+xml":
if name<>'html':
self.log(HtmlFragment({"parent":self.parent.name, "element":self.name,"value":self.value, "type":self.type}))
elif qname not in ["http://www.w3.org/1999/xhtml"]:
self.log(NotHtml({"parent":self.parent.name, "element":self.name, "message":"unexpected namespace: %s" % qname}))
if self.attrs.has_key((None,"mode")):
if self.attrs.getValue((None,"mode")) == 'escaped':
self.log(NotEscaped({"parent":self.parent.name, "element":self.name}))
if name=="div" and qname=="http://www.w3.org/1999/xhtml":
handler=diveater()
else:
handler=eater()
self.children.append(handler)
self.push(handler, name, attrs)
# treat xhtml:div as part of the content for purposes of detecting escaped html
class diveater(eater):
def __init__(self):
eater.__init__(self)
self.mixed = False
def textOK(self):
pass
def characters(self, string):
validatorBase.characters(self, string)
def startElementNS(self, name, qname, attrs):
if not qname:
self.log(MissingNamespace({"parent":"xhtml:div", "element":name}))
self.mixed = True
eater.startElementNS(self, name, qname, attrs)
def validate(self):
if not self.mixed: self.parent.value += self.value
class content(textConstruct):
def maptype(self):
if self.type == 'multipart/alternative':
self.log(InvalidMIMEType({"parent":self.parent.name, "element":self.name, "attr":"type", "value":self.type}))

View file

@ -1,50 +0,0 @@
Options +ExecCGI -MultiViews
AddHandler cgi-script .cgi
AddType image/x-icon ico
RewriteRule ^check$ check.cgi
DirectoryIndex index.html check.cgi
# MAP - bad bots are killing the server (everybody links to their validation results and the bots dutifully crawl it)
RewriteEngine on
RewriteCond %{HTTP_USER_AGENT} BecomeBot [OR]
RewriteCond %{HTTP_USER_AGENT} Crawler [OR]
RewriteCond %{HTTP_USER_AGENT} FatBot [OR]
RewriteCond %{HTTP_USER_AGENT} Feed24 [OR]
RewriteCond %{HTTP_USER_AGENT} Gigabot [OR]
RewriteCond %{HTTP_USER_AGENT} Googlebot [OR]
RewriteCond %{HTTP_USER_AGENT} htdig [OR]
RewriteCond %{HTTP_USER_AGENT} HttpClient
RewriteCond %{HTTP_USER_AGENT} HTTrack [OR]
RewriteCond %{HTTP_USER_AGENT} IQSearch [OR]
RewriteCond %{HTTP_USER_AGENT} msnbot [OR]
RewriteCond %{HTTP_USER_AGENT} NaverBot [OR]
RewriteCond %{HTTP_USER_AGENT} OmniExplorer [OR]
RewriteCond %{HTTP_USER_AGENT} SietsCrawler [OR]
RewriteCond %{HTTP_USER_AGENT} Thunderbird [OR]
RewriteCond %{HTTP_USER_AGENT} TurnitinBot [OR]
RewriteCond %{HTTP_USER_AGENT} User-Agent [OR]
RewriteCond %{HTTP_USER_AGENT} Yahoo!.Slurp [OR]
RewriteRule check - [F,L]
# fastcgi
RewriteCond /home/rubys/public_html/fvstat/status -f
RewriteRule check.cgi(.*)$ http://localhost:8080/rubys/feedvalidator.org/$1 [P]
<Files check.cgi>
Deny from feeds01.archive.org
Deny from feedvalidator.org
Deny from new.getfilesfast.com
Deny from gnat.yodlee.com
Deny from master.macworld.com
Deny from 62.244.248.104
Deny from 207.97.204.219
Deny from ik63025.ikexpress.com
Deny from 65-86-180-70.client.dsl.net
Deny from vanadium.sabren.com
</Files>
<Files config.py>
ForceType text/plain
</Files>

View file

@ -1,26 +0,0 @@
The Feed Validator (includig all code, tests, and documentation) is released
under the following license:
----- begin license block -----
Copyright (c) 2002-2006, Sam Ruby, Mark Pilgrim, Joseph Walton, and Phil Ringnalda
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
----- end license block -----

View file

@ -1,9 +0,0 @@
Some tests, and some functionality, will not be enabled unless a full set
of 32-bit character encodings are available through Python.
The package 'iconvcodec' provides the necessary codecs, if your underlying
operating system supports them. Its web page is at
<http://cjkpython.i18n.org/#iconvcodec>, and a range of packages are
provided.
Python 2.3.x is required, for its Unicode support.

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<definitions
targetNamespace="http://feedvalidator.org/"
xmlns:validator="http://feedvalidator.org/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.xmlsoap.org/wsdl/">
<types>
<xsd:schema elementFormDefault="qualified"
targetNamespace="http://feedvalidator.org/">
<xsd:complexType name="Request">
<xsd:sequence>
<xsd:any namespace="##other"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Message">
<xsd:sequence>
<xsd:element name="level" type="xsd:string"/>
<xsd:element name="type" type="xsd:string"/>
<xsd:element name="line" type="xsd:string"/>
<xsd:element name="column" type="xsd:string"/>
<xsd:element name="msgcount" type="xsd:string"/>
<xsd:element name="text" type="xsd:string"/>
<xsd:any minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="Response">
<xsd:sequence>
<xsd:element name="message" type="validator:Message"
minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="request" type="validator:Request"/>
<xsd:element name="response" type="validator:Response"/>
</xsd:schema>
</types>
<message name="validateIn">
<part name="request" element="validator:request" />
</message>
<message name="validateOut">
<part name="response" element="validator:response" />
</message>
<portType name="RSSValidatorSoap">
<operation name="validate">
<input message="validator:validateIn" />
<output message="validator:validateOut" />
</operation>
</portType>
<binding name="soap" type="validator:RSSValidatorSoap">
<soap:binding transport="http://schemas.xmlsoap.org/soap/http"
style="document" />
<operation name="validate">
<soap:operation soapAction="urn:validate" style="document" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<service name="RSSValidator">
<port name="RSSValidatorSoap" binding="validator:soap">
<soap:address location="http://feedvalidator.org/" />
</port>
</service>
</definitions>

Some files were not shown because too many files have changed in this diff Show more